Skip to content

Fix inf score from division by zero in absolute price scaling#229

Open
sniper-noob wants to merge 3 commits intomode-network:mainfrom
sniper-noob:fix/inf-score-from-abs-price-divzero
Open

Fix inf score from division by zero in absolute price scaling#229
sniper-noob wants to merge 3 commits intomode-network:mainfrom
sniper-noob:fix/inf-score-from-abs-price-divzero

Conversation

@sniper-noob
Copy link
Copy Markdown
Contributor

Problem:
When real_price_path[-1] is 0 (or NaN), the absolute price scaling at crps_calculation.py line 99 divides by zero, producing an inf score. The safety check at reward.py uses np.isnan(score), which does NOT catch inf (np.isnan(inf) == False). The inf score passes through to compute_prompt_scores where it poisons np.percentile(scores, 90), making percentile90 = inf. This destroys the capping mechanism and concentrates nearly all reward weight on a single miner.

Example:
10 miners with scores [50, 60, 70, 80, 90, 100, 110, 120, inf, 150]
percentile90 = inf (should be ~135)
np.minimum(scores, inf) = scores (no capping applied)
After softmax: miner 0 gets 86.5% of all rewards
Everyone else is suppressed, regardless of their actual quality

Fix:
crps_calculation.py line 97-100:
Guard absolute price scaling — skip block if last_price is 0 or
not finite, instead of dividing by it

reward.py lines 108, 422:
np.isnan(score) -> not np.isfinite(score)
Catches both NaN AND inf, returning -1 as intended

Problem:
When real_price_path[-1] is 0 (or NaN), the absolute price scaling at
crps_calculation.py line 99 divides by zero, producing an inf score.
The safety check at reward.py uses np.isnan(score), which does NOT
catch inf (np.isnan(inf) == False). The inf score passes through to
compute_prompt_scores where it poisons np.percentile(scores, 90),
making percentile90 = inf. This destroys the capping mechanism and
concentrates nearly all reward weight on a single miner.

Example:
  10 miners with scores [50, 60, 70, 80, 90, 100, 110, 120, inf, 150]
  percentile90 = inf (should be ~135)
  np.minimum(scores, inf) = scores (no capping applied)
  After softmax: miner 0 gets 86.5% of all rewards
  Everyone else is suppressed, regardless of their actual quality

Fix:
  crps_calculation.py line 97-100:
    Guard absolute price scaling — skip block if last_price is 0 or
    not finite, instead of dividing by it

  reward.py lines 108, 422:
    np.isnan(score) -> not np.isfinite(score)
    Catches both NaN AND inf, returning -1 as intended

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sniper-noob sniper-noob force-pushed the fix/inf-score-from-abs-price-divzero branch from 23f9eba to daac5a0 Compare March 11, 2026 09:02
Flake8 C901: calculate_crps_for_miner exceeded max-complexity=10.
Extracted block CRPS loop into helper, which also contains the
div-by-zero guard for absolute price scaling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Thykof
Copy link
Copy Markdown
Member

Thykof commented Mar 16, 2026

Hi @sniper-noob thanks for the contribution, can you add type hint please?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a reward-poisoning failure mode where CRPS absolute-price scaling could divide by a zero/NaN final real price, producing inf scores that bypassed NaN-only checks and broke percentile-based capping in reward computation.

Changes:

  • Add a guard in CRPS absolute-price scaling to skip scaling when the final real price is 0 or non-finite.
  • Treat non-finite CRPS scores (NaN and inf) as invalid in the reward worker (np.isfinite).
  • Add regression tests covering absolute-price last-real 0/NaN cases and mixed-interval scoring.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
synth/validator/crps_calculation.py Extracts per-block CRPS computation and adds a non-finite/zero last-price guard for absolute-price scaling.
synth/validator/reward.py Hardens worker validation to reject non-finite CRPS scores (handles inf as well as NaN).
tests/test_calculate_crps.py Adds regression tests to ensure CRPS scores stay finite when the last real price is 0/NaN.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +13 to +15
simulated_changes, real_changes, data_blocks, absolute_price, last_price
):
"""Compute CRPS for observed blocks, returns (total, details list)."""
Comment on lines +532 to +548
# 10 normal scores + 1 that would be inf without fix
scores = np.array([50, 60, 70, 80, 90, 100, 110, 120, 130, 150])
prompt_scores, p90, lowest = compute_prompt_scores(scores)
self.assertTrue(np.all(np.isfinite(prompt_scores)))
self.assertTrue(np.isfinite(p90))

# With inf injected (simulating old bug)
scores_with_inf = np.array(
[50, 60, 70, 80, 90, 100, 110, 120, np.inf, 150]
)
ps_inf, p90_inf, _ = compute_prompt_scores(scores_with_inf)
# p90 with inf is itself inf, destroying capping
self.assertFalse(
np.isfinite(p90_inf),
"This proves inf poisons percentile90",
)

- Add full type annotations to _compute_block_crps function signature
- Add type hints to calculate_price_changes_over_intervals boolean parameters
- Improves code clarity per owner request
@sniper-noob
Copy link
Copy Markdown
Contributor Author

Hi @sniper-noob thanks for the contribution, can you add type hint please?

@Thykof done:)

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.

3 participants