Repository navigation
petab1to2: order placeholders by index, not as text - #532
Conversation
v1 observable/noise parameter overrides are positional, but the v2 `observablePlaceholders` / `noisePlaceholders` were sorted as strings, so from 10 placeholders on (`observableParameter10_x` < `observableParameter2_x`) the overrides were silently assigned to the wrong placeholders. Sort by the numeric index instead, and raise a ValueError if the placeholders are not numbered consecutively from 1, since positional overrides cannot be mapped then (v1 lint already rejects this). Closes #531 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #532 +/- ##
==========================================
- Coverage 76.63% 76.63% -0.01%
==========================================
Files 67 67
Lines 7529 7527 -2
Branches 1342 1342
==========================================
- Hits 5770 5768 -2
Misses 1262 1262
Partials 497 497 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟢 Approval recommended
The localized fix preserves positional mapping, with focused regression coverage and no unresolved findings.
0 open findings
What changed in this PR
Fixes #531 by preserving positional override mapping during PEtab v1-to-v2 conversion.
Changes:
- Sorts observable and noise placeholders by numeric index.
- Rejects placeholder numbering that is not consecutive from 1.
- Adds regression tests for 10+ placeholders and numbering gaps.
| File | Description |
|---|---|
| tests/v2/test_conversion.py | Tests override alignment and gap rejection for both placeholder types. |
| petab/v2/petab1to2.py | Sorts placeholders numerically and validates consecutive numbering. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
| if [i for i, _ in placeholders] != list( | ||
| range(1, len(placeholders) + 1) | ||
| ): | ||
| raise ValueError( | ||
| f"The {type_} placeholders of observable " | ||
| f"`{row[v1.C.OBSERVABLE_ID]}' are not numbered consecutively " | ||
| f"starting from 1: {[p for _, p in placeholders]}." | ||
| ) | ||
| return v2.C.PARAMETER_SEPARATOR.join(p for _, p in placeholders) |
There was a problem hiding this comment.
Instead of re-implementing here, can the logic from the linter be called here? Fine as is, if it's not straightforward.
Also ideally the same check isn't applied to a problem twice, which this PR might introduce based on
In
petab1to2, v1 linting already rejects such problems ("Non-consecutive numbering of placeholder parameter"). The new check covers direct calls tov1v2_observable_df.
So we could move all checks from petab1to2 to the v1v2_* methods? Or if this is the only v1v2_ method where it is relevant to recheck, then fine.
There was a problem hiding this comment.
Good point, done in bfaf9e4: extract_placeholders now calls v1.get_formula_placeholders, the helper the v1 linter uses. It returns the placeholders ordered by index and raises if they aren't numbered 1..n; that error is re-raised as a ValueError naming the observable. As a side effect, placeholders are now found the same way as in v1's override-count check (by text match on the formula rather than sympy's free symbols, which drop a placeholder that cancels out).
On applying the check twice: there is no separate check anymore. The conversion needs the index-ordered list anyway, and the error comes with it. I'd still keep it in v1v2_observable_df rather than rely on the linter alone, because #534 adds validate=False, which skips v1 linting entirely. Without this, that path would silently produce a misaligned placeholder list. I wouldn't move the other lint checks into the v1v2_* functions, since many of them compare tables against each other (overrides vs. observables, parameter table vs. model), which a single-table function can't see.
Use the helper that the v1 linter and the v1 override-count check use, instead of re-implementing the index ordering and the consecutive-numbering check. This also makes the placeholders consistent with v1 when sympy would cancel a placeholder from the formula. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eholder-sort-5c5d23 # Conflicts: # petab/v2/petab1to2.py
The merge of #532 removed the last use of `re`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
petab1to2sorted an observable's placeholders as text when building the v2observablePlaceholders/noisePlaceholderscolumns. The v1 overrides in the measurement table are positional, so from 10 placeholders on (observableParameter10_xsorts beforeobservableParameter2_x), the override values were silently assigned to the wrong placeholders.Changes:
v1.get_formula_placeholders, the helper the v1 linter uses. It orders them by the index in their ID.observableParameter1_xandobservableParameter3_x), raise aValueError. The positional overrides can't be mapped in that case. Inpetab1to2, v1 linting already rejects such problems ("Non-consecutive numbering of placeholder parameter"). The new check covers direct calls tov1v2_observable_df.Tests:
test_petab1to2_placeholder_order: converts a problem with 12 observable placeholders and 11 noise placeholders, and checks that each placeholder gets its own override value.test_v1v2_observable_df_placeholder_gap: checks that a skipped index raises the error, for observable and noise placeholders.The repro from the issue now prints
observableParameter<i>_obs_B = ifor all 11 placeholders.Closes #531
🤖 Generated with Claude Code