Skip to content

Flip ResultFloat/ResultVec/ResultBool default from 0 to NaN#952

Merged
blooop merged 1 commit into
mainfrom
flip-result-default-to-nan
Jun 11, 2026
Merged

Flip ResultFloat/ResultVec/ResultBool default from 0 to NaN#952
blooop merged 1 commit into
mainfrom
flip-result-default-to-nan

Conversation

@blooop

@blooop blooop commented Jun 3, 2026

Copy link
Copy Markdown
Owner

What

Flip the default value of ResultFloat, ResultVec, and ResultBool from 0 to NaN.

Why

A 0/False default makes an unrecorded sample — a run that aborts before measuring, or a result var the worker never sets — indistinguishable from a real 0/False measurement, dragging nan-aware regression/aggregation means toward zero. The storage layer (result_collector.setup_dataset) already initialises result arrays with np.nan; the 0 param default was the value that overwrote that "unwritten" sentinel.

After this change, unrecorded samples stay NaN and are dropped by the existing nan-aware reductions (skipna=True xarray reductions, _clean_1d in regression.py, np.nanmean/nansum in the agg registry, df.dropna() in the Optuna path).

Scope / decisions

  • ResultBool is now included. An earlier draft kept it at 0/False, justified by the binomial-std calc treating bool means as proportions over a fixed repeat count. That rationale is now obsolete: the binomial SE was fixed on main to divide p*(1-p) by the per-cell count of valid (non-NaN) repeats, so missing repeats no longer understate it. The semantics are missing ≠ failure: an unrecorded repeat is dropped from the success proportion rather than counted as False. A worker that wants a crash/abort to count as a failure must explicitly record False on its failure path.
  • Callers wanting the old zero/False-fill behaviour opt out with default=0.
  • CACHE_VERSION is deliberately NOT bumped. The result-var default is not part of BenchCfg.hash_persistent(), so existing benchmark and over_time history caches are preserved (no forced global wipe). The trade-off: the new NaN default only applies to cache misses; already-cached cells keep whatever sentinel they were stored with, so a benchmark with missing samples may transiently hold a mix of 0 (old) and NaN (new) until those cells are recomputed. For rolling over_time history this self-heals as old samples age out of the window.

Verification

pixi run ci green (1474 passed, 5 skipped); format/lint clean (pylint 10.00/10). No allow_nan=False JSON sinks exist; the report-export contract coerces non-finite floats to null via _finite_or_none.

🤖 Generated with Claude Code

@sourcery-ai

sourcery-ai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Switches the default value for numeric result variables from 0 to NaN so unrecorded samples are treated as missing instead of real zeros, updates tests/docs/cache-version and golden hashes accordingly, and keeps ResultBool semantics unchanged.

File-Level Changes

Change Details Files
Change ResultFloat and ResultVec defaults from 0 to NaN while preserving an explicit opt-out to 0 and keeping ResultBool at 0/False.
  • Update ResultFloat.init default parameter from 0 to float('nan') and adjust documentation comment to explain NaN as the missing-sample sentinel.
  • Ensure ResultBool explicitly resets default to 0 and document why bool defaults remain False, including interaction with binomial-std calculations.
  • Update ResultVec.init default parameter from 0 to float('nan') and revise its documentation comment to match ResultFloat semantics, noting the ability to opt out with default=0.
bencher/variables/results.py
Adjust tests to validate the new NaN defaults and the explicit 0 opt-out path.
  • Change UnrecordedZeroBench fixture to explicitly pass default=0 to ResultFloat to model old zero-fill behaviour.
  • Replace tests asserting zero defaults for ResultFloat/ResultVec with tests asserting NaN defaults.
  • Add tests that explicit default=0 and other numeric defaults continue to be honoured for ResultFloat and ResultVec.
test/test_result_nan_default.py
Document the breaking change and bump cache and project versions so old zero-filled caches and config hashes cannot mix with new NaN semantics.
  • Add a 1.103.0 changelog entry describing the default change, its rationale, the ResultBool exception, and the cache invalidation.
  • Update the golden BenchCfg hash constants to match the new configuration/hash inputs.
  • Bump CACHE_VERSION from '3' to '4' to invalidate benchmark and over_time caches and prevent mixing old and new result semantics.
  • Bump the project version in pyproject.toml from 1.102.0 to 1.103.0.
CHANGELOG.md
test/test_hash_persistent.py
bencher/cache_management.py
pyproject.toml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've found 1 issue, and left some high level feedback:

  • Now that ResultFloat/ResultVec default to NaN, consider centralising and explicitly handling NaN in any JSON-serialisation layer (e.g. normalising to a consistent representation rather than relying on default encoder behaviour) so callers don’t run into subtle consumer issues with non-JSON-safe values.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Now that `ResultFloat`/`ResultVec` default to `NaN`, consider centralising and explicitly handling `NaN` in any JSON-serialisation layer (e.g. normalising to a consistent representation rather than relying on default encoder behaviour) so callers don’t run into subtle consumer issues with non-JSON-safe values.

## Individual Comments

### Comment 1
<location path="test/test_result_nan_default.py" line_range="60-69" />
<code_context>


 class TestNanDefaultConstruction(unittest.TestCase):
-    def test_default_is_zero_for_backward_compat(self):
-        self.assertEqual(ResultFloat().default, 0)
-        self.assertEqual(ResultVec(size=2).default, 0)
+    def test_default_is_nan(self):
+        self.assertTrue(math.isnan(ResultFloat().default))
+        self.assertTrue(math.isnan(ResultVec(size=2).default))

     def test_default_can_be_nan(self):
         self.assertTrue(math.isnan(ResultFloat(default=float("nan")).default))
         self.assertTrue(math.isnan(ResultVec(size=2, default=float("nan")).default))

+    def test_explicit_zero_default_opt_out(self):
+        self.assertEqual(ResultFloat(default=0).default, 0)
+        self.assertEqual(ResultVec(size=2, default=0).default, 0)
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for ResultBool to lock in its intentionally different default (0/False) and ensure it does not regress to NaN.

Please add assertions that capture this behaviour, for example:

- `self.assertEqual(ResultBool().default, 0)` and `self.assertFalse(ResultBool().default)`
- Optionally, `self.assertFalse(math.isnan(ResultBool().default))` to guard against an accidental switch to NaN.

That will make the intended difference between numeric and boolean defaults explicit in the tests and help prevent future regressions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread test/test_result_nan_default.py
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown

Performance Report for 91eae7d

Metric Value
Total tests 1451
Total time 117.38s
Mean 0.0809s
Median 0.0010s
Top 10 slowest tests
Test Time (s)
test.test_bench_examples.TestBenchExamples::test_example_meta 15.620
test.test_over_time_save_perf::test_save_faster_without_aggregated_tab 5.040
test.test_hash_persistent.TestCrossProcessDeterminism::test_hash_stable_across_two_processes[ResultBool] 3.961
test.test_split_render_examples::test_split_render_subprocess_media 3.841
test.test_generated_examples::test_generated_example[cartesian_animation/example_cartesian_animation.py] 3.108
test.test_generated_examples::test_generated_example[regression/example_regression_tuning_drift.py] 2.782
test.test_generated_examples::test_generated_example[result_types/result_image/example_result_image_to_video.py] 2.694
test.test_generated_examples::test_generated_example[regression/example_regression_tuning_step.py] 2.637
test.test_generated_examples::test_generated_example[regression/example_regression_tuning_noise.py] 2.261
test.test_over_time_repeats.TestMaxSliderPoints::test_default_subsampling_caps_at_max 2.214

Full report

Updated by Performance Tracking workflow

@blooop

blooop commented Jun 4, 2026

Copy link
Copy Markdown
Owner Author

Addressed Sourcery review (commit 91eae7d):

  • Add ResultBool default tests ✅ — added test_bool_default_stays_false_not_nan asserting ResultBool().default == 0, falsy, and not-NaN, locking in the intentionally-different boolean default and guarding against an accidental flip to NaN.
  • Centralise NaN in the JSON-serialisation layer — not applicable: bencher does not serialise results via json. Collected results are pickled (save_result/load_result) and plots are serialised by bokeh (which encodes NaN as null). Both paths are covered by the serialization round-trip tests added in this PR (TestNanDefaultSerialization), so a NaN default is already handled end-to-end.

@blooop blooop force-pushed the flip-result-default-to-nan branch from 91eae7d to 85af908 Compare June 11, 2026 10:09
@blooop blooop changed the title Flip ResultFloat/ResultVec default from 0 to NaN Flip ResultFloat/ResultVec/ResultBool default from 0 to NaN Jun 11, 2026
@github-actions

Copy link
Copy Markdown

Performance Report for 85af908

Metric Value
Total tests 1479
Total time 126.97s
Mean 0.0858s
Median 0.0020s
Top 10 slowest tests
Test Time (s)
test.test_bench_examples.TestBenchExamples::test_example_meta 18.286
test.test_over_time_save_perf::test_save_faster_without_aggregated_tab 5.551
test.test_generated_examples::test_generated_example[regression/example_regression_tuning_drift.py] 4.644
test.test_generated_examples::test_generated_example[cartesian_animation/example_cartesian_animation.py] 3.257
test.test_split_render_examples::test_split_render_subprocess_media 3.164
test.test_hash_persistent.TestCrossProcessDeterminism::test_hash_stable_across_two_processes[ResultBool] 2.879
test.test_generated_examples::test_generated_example[result_types/result_image/example_result_image_to_video.py] 2.839
test.test_generated_examples::test_generated_example[regression/example_regression_tuning_step.py] 2.830
test.test_generated_examples::test_generated_example[regression/example_regression_tuning_noise.py] 2.797
test.test_over_time_repeats.TestMaxSliderPoints::test_default_subsampling_caps_at_max 2.468

Full report

Updated by Performance Tracking workflow

An *unrecorded* sample — a run that aborts before measuring, or a result
var the worker never sets — was indistinguishable from a real 0/False
measurement, dragging nan-aware regression/aggregation means toward zero.
The storage layer already initialises result arrays with NaN; the 0 param
default was the value that overwrote that "unwritten" sentinel.

Flip the default to NaN for ResultFloat, ResultVec, and ResultBool so
unrecorded samples stay missing and are dropped by the existing nan-aware
reductions (skipna xarray reductions, _clean_1d in regression, np.nanmean/
nansum in the agg registry, df.dropna() in the Optuna path). Callers
wanting the old zero/False fill opt out with default=0.

ResultBool is included: missing is not a recorded failure, so an unrecorded
repeat is dropped from the success proportion rather than counted as False.
The binomial SE already divides p*(1-p) by the per-cell count of valid
(non-NaN) repeats, so missing repeats don't understate it. A worker that
wants a crash/abort to count as failure must record False on its failure
path.

CACHE_VERSION is deliberately NOT bumped: default is not part of
BenchCfg.hash_persistent(), so existing benchmark and over_time history
caches are preserved. The new NaN default applies only to cache misses;
already-cached cells keep their stored value, so a missing-sample benchmark
may transiently mix 0 (old) and NaN (new) until those cells recompute.
Version 1.104.2 -> 1.105.0.
@blooop blooop force-pushed the flip-result-default-to-nan branch from 85af908 to 992f181 Compare June 11, 2026 11:11
@github-actions

Copy link
Copy Markdown

Performance Report for 992f181

Metric Value
Total tests 1479
Total time 120.52s
Mean 0.0815s
Median 0.0020s
Top 10 slowest tests
Test Time (s)
test.test_bench_examples.TestBenchExamples::test_example_meta 16.647
test.test_over_time_save_perf::test_save_faster_without_aggregated_tab 5.281
test.test_generated_examples::test_generated_example[regression/example_regression_tuning_drift.py] 4.500
test.test_generated_examples::test_generated_example[cartesian_animation/example_cartesian_animation.py] 3.174
test.test_split_render_examples::test_split_render_subprocess_media 2.864
test.test_generated_examples::test_generated_example[regression/example_regression_tuning_step.py] 2.784
test.test_generated_examples::test_generated_example[regression/example_regression_tuning_noise.py] 2.761
test.test_hash_persistent.TestCrossProcessDeterminism::test_hash_stable_across_two_processes[ResultBool] 2.755
test.test_generated_examples::test_generated_example[result_types/result_image/example_result_image_to_video.py] 2.712
test.test_over_time_repeats.TestMaxSliderPoints::test_default_subsampling_caps_at_max 2.375

Full report

Updated by Performance Tracking workflow

@blooop blooop merged commit a0c4d4c into main Jun 11, 2026
8 checks passed
@blooop blooop deleted the flip-result-default-to-nan branch June 11, 2026 15:07
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