Skip to content

fix: detect_signals() on phased charts; value='response' (#141, #142) - #145

Merged
cnicholas merged 2 commits into
perf/stratified-linearfrom
fix/phased-signals
Oct 14, 2026
Merged

cnicholas merged 2 commits into
perf/stratified-linearfrom
fix/phased-signals

Conversation

@cnicholas

Copy link
Copy Markdown
Owner

Summary

Two bugs found while scoping phased Xbar/S (#144), both fixed for 0.4.0. Stacked on #138.

  • detect_signals() raises TypeError on every phased chart #141, detect_signals() works on phased charts.
    • Before: it raised TypeError on every phased=True chart. Phased statistics carry center=None (each phase has its own centre), and the per-row zone calculation subtracted that scalar. The chart marks run rules not applicable, but only the renderer read the flag.
    • Now: phased charts get Rule 1 by default, judged against each phase's limits, and report the same points the chart flags. An explicit rule list runs as given, against each row's own centre.
  • execute(value='response') and why_not(..., value='response') refuse the documented value #142, value='response' charts the response.
    • Before: it is in the execute() signature and docstring, but every chart refused it ("Valid charts for RESPONSE: None"), because residuals are told apart by a leading R.
    • Now: execute(), supports_calibration() and why_not() treat it, in any case, as value=None.

Contract / invariants

  • Limits, statistics and the points charts flag are unchanged. Signal detection only reads them.
  • Unphased charts keep their rules: X/mR evaluate rules 1–8, Xbar/S Rule 1.
  • value=None behaves exactly as before. recentered=True with the response is still refused, now with a message that says why.
  • No residual, design-state or limit computation touched.

Tests

  • tests/test_phased_limits.py::TestPhasedSignals:
    • combined, companion and stratified (focus) phased charts;
    • violation records carry the phase's centre and limits;
    • explicit rules 2 and 4 are judged against each phase's centre;
    • unphased X keeps every rule.
  • tests/test_execute_validation.py::TestValueResponse:
    • 'response', 'Response' and 'RESPONSE' equal value=None on Xbar, S, Xbar+S, X, mR and Histogram (charts, statistics, metadata);
    • why_not and supports_calibration agree;
    • recentered stays refused.
  • Both new test classes fail on the parent commit.

Verification

  • Full suite: 2,551 tests collected (27 new), no failures (exit 0).
  • ruff check .: clean.
  • e2e gate: 1547 pass / 0 fail / 4 pending, and docs/reference/validation.md is unchanged when regenerated.

Closes #141
Closes #142

detect_signals() raised TypeError on every phased=True chart. Each phase has its own centre,
so the statistics carry center=None, and the per-row zone calculation subtracted that scalar.
The chart marks run rules not applicable (metadata run_rules_applicable=False, 70503be), but
only the renderer read the flag.

- result_signals passes run_rules_applicable to SignalDetector.detect. When it is False and
  the rules are the chart-type default, the default is Rule 1 only: rules 2-8 are not
  applicable rather than skipped, so the result is not marked partial on their account.
- An explicit rule list runs as given (SignalConfig's "explicit rules, no filtering"
  contract). The per-row zones, the run rule (rule 4) and the violation records read each
  row's own centre when the scalar centre is None.

The limits and the points the chart flags were always correct; Rule 1 now reports the same
points (flagged_observations equals the rows with beyond_limits != 0). Unphased charts are
unchanged: X still evaluates rules 1-8, Xbar/S Rule 1.

tests/test_phased_limits.py TestPhasedSignals: combined, companion and stratified (via
focus) phased charts; the violation records carry the phase's centre and limits; explicit
rules 2 and 4 judged against each phase's centre; unphased X keeps every rule.
execute(value='response') is in the signature (Literal['response', 'R1', ...]) and the
docstring, but every chart refused it: "Valid charts for RESPONSE: None".
_validate_execute_request tells residuals apart by value.upper().startswith('R'), which
'RESPONSE' also satisfies, so the value went down the residual path and failed the pairing
check. why_not() and supports_calibration() refused it the same way.

_normalise_value turns 'response' (any case) into None, the response's own spelling, at the
start of execute(), supports_calibration() and why_not(); _resolve_value_column already
treats None as the response. recentered=True with the response is still refused, and the
message now says why: the response is already on the measurement scale.

tests/test_execute_validation.py TestValueResponse: 'response', 'Response' and 'RESPONSE'
give the same charts, statistics and metadata as value=None on Xbar, S, Xbar+S, X, mR and
Histogram; why_not and supports_calibration agree; recentered stays refused.
@cnicholas
cnicholas merged commit 95b3473 into perf/stratified-linear Oct 14, 2026
17 checks passed
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.

1 participant