Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions gittensor/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ class PullRequest:
base_score: float = 0.0
issue_multiplier: float = 1.0
open_pr_spam_multiplier: float = 1.0
repository_uniqueness_multiplier: float = 1.0
pioneer_multiplier: float = 1.0
pioneer_rank: int = 0
time_decay_multiplier: float = 1.0
credibility_multiplier: float = 1.0
raw_credibility: float = 1.0 # Before applying ^k scalar
Expand Down Expand Up @@ -194,7 +195,7 @@ def calculate_final_earned_score(self) -> float:
'repo': self.repo_weight_multiplier,
'issue': self.issue_multiplier,
'spam': self.open_pr_spam_multiplier,
'unique': self.repository_uniqueness_multiplier,
'pioneer': self.pioneer_multiplier,
'decay': self.time_decay_multiplier,
'cred': self.credibility_multiplier,
}
Expand All @@ -203,7 +204,11 @@ def calculate_final_earned_score(self) -> float:

# Log all multipliers (credibility shows ^k format)
mult_str = ' × '.join(
f'cred={self.raw_credibility:.2f}^{self.credibility_scalar}' if k == 'cred' else f'{k}={v:.2f}'
f'cred={self.raw_credibility:.2f}^{self.credibility_scalar}'
if k == 'cred'
else f'pioneer={v:.2f}({"P" if self.pioneer_rank == 1 else "F"})'
if k == 'pioneer'
else f'{k}={v:.2f}'
for k, v in multipliers.items()
)
bt.logging.info(
Expand Down
3 changes: 2 additions & 1 deletion gittensor/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
DEFAULT_MAX_CONTRIBUTION_SCORE_FOR_FULL_BONUS = 2000

# Boosts
UNIQUE_PR_BOOST = 0.74
PIONEER_BASE_BONUS = 1.5 # Bonus for pioneer (rank-1 only, 2.5x total multiplier)
PIONEER_MULTI_REPO_DAMPING = 0.5 # Exponent for diminishing returns when pioneering many repos
MAX_CODE_DENSITY_MULTIPLIER = 3.0

# Issue boosts
Expand Down
123 changes: 91 additions & 32 deletions gittensor/validator/evaluation/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import math
from datetime import datetime, timezone
from typing import Dict, Optional
from typing import Dict, Optional, Tuple

import bittensor as bt

Expand All @@ -20,13 +20,14 @@
MAX_OPEN_PR_THRESHOLD,
MIN_TOKEN_SCORE_FOR_BASE_SCORE,
OPEN_PR_THRESHOLD_TOKEN_SCORE,
PIONEER_BASE_BONUS,
PIONEER_MULTI_REPO_DAMPING,
SECONDS_PER_DAY,
SECONDS_PER_HOUR,
TIME_DECAY_GRACE_PERIOD_HOURS,
TIME_DECAY_MIN_MULTIPLIER,
TIME_DECAY_SIGMOID_MIDPOINT,
TIME_DECAY_SIGMOID_STEEPNESS_SCALAR,
UNIQUE_PR_BOOST,
)
from gittensor.utils.github_api_tools import (
FileContentPair,
Expand Down Expand Up @@ -226,25 +227,86 @@ def calculate_pr_multipliers(
pr.credibility_multiplier = 1.0


def count_repository_contributors(miner_evaluations: Dict[int, MinerEvaluation]) -> Dict[str, int]:
def calculate_pioneer_ranks(
miner_evaluations: Dict[int, MinerEvaluation],
) -> Tuple[Dict[str, Dict[int, int]], Dict[str, Dict[int, int]]]:
"""For each repo, determine pioneer rank of each contributing miner.

Ranks are based on the earliest merged PR date per miner per repo.
Ties on the same timestamp are broken by lower PR number (deterministic,
single-winner — avoids multiplying the bonus when merge bots batch-process
a queue at the same second).

Quality gate: PRs without tier configuration or with token_score below
MIN_TOKEN_SCORE_FOR_BASE_SCORE are excluded from ranking. This prevents
low-quality snipe PRs from capturing pioneer status.

Returns:
Tuple of:
- ranks: {repo: {uid: rank}} where rank=1 is the pioneer
- pioneer_prs: {repo: {uid: pr_number}} the earliest eligible PR per (repo, uid)
"""
Count how many miners contribute to each repository and log statistics.
# Collect earliest eligible merge per (repo, uid), tracking both date and PR number
repo_earliest: Dict[str, Dict[int, Tuple[datetime, int]]] = {}

for uid, evaluation in miner_evaluations.items():
for pr in evaluation.merged_pull_requests:
if not pr.merged_at or pr.repository_tier_configuration is None:
continue
if pr.token_score < MIN_TOKEN_SCORE_FOR_BASE_SCORE:
continue
repo = pr.repository_full_name
if repo not in repo_earliest:
repo_earliest[repo] = {}
current = repo_earliest[repo].get(uid)
if current is None or pr.merged_at < current[0] or (pr.merged_at == current[0] and pr.number < current[1]):
repo_earliest[repo][uid] = (pr.merged_at, pr.number)

# Assign ranks per repo (sorted by merge date, then PR number for tie-breaking)
pioneer_ranks: Dict[str, Dict[int, int]] = {}
pioneer_prs: Dict[str, Dict[int, int]] = {}

for repo, uid_entries in repo_earliest.items():
sorted_entries = sorted(uid_entries.items(), key=lambda x: x[1])
pioneer_ranks[repo] = {uid: rank for rank, (uid, _) in enumerate(sorted_entries, 1)}
pioneer_prs[repo] = {uid: pr_number for uid, (_, pr_number) in uid_entries.items()}

if pioneer_ranks:
bt.logging.info(f'Pioneer ranks computed for {len(pioneer_ranks)} repositories')
for repo, ranks in sorted(pioneer_ranks.items()):
rank_str = ', '.join(f'UID {uid}: rank {r}' for uid, r in sorted(ranks.items(), key=lambda x: x[1]))
bt.logging.debug(f' {repo}: {rank_str}')

return pioneer_ranks, pioneer_prs


def count_pioneered_repos(pioneer_ranks: Dict[str, Dict[int, int]]) -> Dict[int, int]:
"""Count how many repositories each miner is rank 1 (pioneer) on.

Returns:
Dict[str, int]: Dictionary mapping repository names to contributor counts
Dict mapping uid -> number of repos pioneered.
"""
repo_counts: Dict[str, int] = {}
counts: Dict[int, int] = {}
for ranks in pioneer_ranks.values():
for uid, rank in ranks.items():
if rank == 1:
counts[uid] = counts.get(uid, 0) + 1
return counts

for evaluation in miner_evaluations.values():
for repo in evaluation.unique_repos_contributed_to:
repo_counts[repo] = repo_counts.get(repo, 0) + 1

if repo_counts:
bt.logging.info(f'Repository contribution counts: {len(repo_counts)} total repositories')
for repo, count in sorted(repo_counts.items(), key=lambda x: -x[1]):
bt.logging.info(f'{repo}: {count}')
def calculate_pioneer_multiplier(rank: int, pioneered_repo_count: int = 1) -> float:
"""Calculate pioneer multiplier (binary: rank 1 only) with multi-repo damping.

return repo_counts
Formula: 1.0 + PIONEER_BASE_BONUS / pioneered_repo_count^PIONEER_MULTI_REPO_DAMPING
Only rank-1 pioneers get a bonus. All other ranks return 1.0.

The multi-repo damping discourages a single miner from racing to pioneer
many repositories: the more repos they pioneer, the less bonus per repo.
"""
if rank != 1:
return 1.0
damping = max(1, pioneered_repo_count) ** PIONEER_MULTI_REPO_DAMPING
return 1.0 + PIONEER_BASE_BONUS / damping


def calculate_open_pr_threshold(
Expand Down Expand Up @@ -305,11 +367,11 @@ def calculate_time_decay_multiplier(pr: PullRequest) -> float:


def finalize_miner_scores(miner_evaluations: Dict[int, MinerEvaluation]) -> None:
"""Finalize all miner scores: apply uniqueness multipliers, calculate totals, and deduct collateral."""
"""Finalize all miner scores: apply pioneer multipliers, calculate totals, and deduct collateral."""
bt.logging.info('**Finalizing miner scores**')

repo_counts = count_repository_contributors(miner_evaluations)
total_contributing_miners = sum(1 for ev in miner_evaluations.values() if ev.unique_repos_contributed_to)
pioneer_ranks, pioneer_prs = calculate_pioneer_ranks(miner_evaluations)
pioneered_repo_counts = count_pioneered_repos(pioneer_ranks)

for uid, evaluation in miner_evaluations.items():
if not evaluation:
Expand Down Expand Up @@ -348,9 +410,17 @@ def finalize_miner_scores(miner_evaluations: Dict[int, MinerEvaluation]) -> None

# Process merged PRs
for pr in evaluation.merged_pull_requests:
pr.repository_uniqueness_multiplier = calculate_uniqueness_multiplier(
pr.repository_full_name, repo_counts, total_contributing_miners
)
repo_ranks = pioneer_ranks.get(pr.repository_full_name, {})
repo_pioneer_prs = pioneer_prs.get(pr.repository_full_name, {})
pr.pioneer_rank = repo_ranks.get(uid, 0)

# Pioneer bonus: only rank-1 pioneer's earliest eligible PR gets the bonus.
is_pioneer_pr = pr.pioneer_rank == 1 and repo_pioneer_prs.get(uid) == pr.number
if is_pioneer_pr:
miner_pioneered = pioneered_repo_counts.get(uid, 1)
pr.pioneer_multiplier = round(calculate_pioneer_multiplier(1, miner_pioneered), 2)
else:
pr.pioneer_multiplier = 1.0

# Apply spam multiplier (calculated once per miner based on unlocked tiers)
pr.open_pr_spam_multiplier = spam_multiplier
Expand Down Expand Up @@ -431,17 +501,6 @@ def finalize_miner_scores(miner_evaluations: Dict[int, MinerEvaluation]) -> None
bt.logging.info('Finalization complete.')


def calculate_uniqueness_multiplier(
repo_full_name: str, repo_counts: Dict[str, int], total_contributing_miners: int
) -> float:
"""Calculate repository uniqueness multiplier based on how many miners contribute to a repo."""
if total_contributing_miners == 0:
return 1.0
repo_count = repo_counts.get(repo_full_name, 0)
uniqueness_score = (total_contributing_miners - repo_count + 1) / total_contributing_miners
return 1.0 + (uniqueness_score * UNIQUE_PR_BOOST)


def calculate_issue_multiplier(pr: PullRequest) -> float:
"""
Calculate PR score multiplier based on the first valid linked issue's age.
Expand Down Expand Up @@ -541,7 +600,7 @@ def calculate_open_pr_collateral_score(pr: PullRequest) -> float:

Applicable multipliers: repo_weight, issue
NOT applicable: time_decay (merge-based), credibility_multiplier (merge-based),
uniqueness (cross-miner), open_pr_spam (not for collateral)
pioneer (cross-miner), open_pr_spam (not for collateral)
"""
from math import prod

Expand Down
5 changes: 3 additions & 2 deletions gittensor/validator/storage/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
number, repository_full_name, uid, hotkey, github_id, title, author_login,
merged_at, pr_created_at, pr_state,
repo_weight_multiplier, base_score, issue_multiplier,
open_pr_spam_multiplier, repository_uniqueness_multiplier, time_decay_multiplier,
open_pr_spam_multiplier, pioneer_multiplier, pioneer_rank, time_decay_multiplier,
credibility_multiplier, raw_credibility, credibility_scalar,
earned_score, collateral_score,
additions, deletions, commits, total_nodes_scored,
Expand All @@ -56,7 +56,8 @@
base_score = EXCLUDED.base_score,
issue_multiplier = EXCLUDED.issue_multiplier,
open_pr_spam_multiplier = EXCLUDED.open_pr_spam_multiplier,
repository_uniqueness_multiplier = EXCLUDED.repository_uniqueness_multiplier,
pioneer_multiplier = EXCLUDED.pioneer_multiplier,
pioneer_rank = EXCLUDED.pioneer_rank,
time_decay_multiplier = EXCLUDED.time_decay_multiplier,
credibility_multiplier = EXCLUDED.credibility_multiplier,
raw_credibility = EXCLUDED.raw_credibility,
Expand Down
3 changes: 2 additions & 1 deletion gittensor/validator/storage/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ def store_pull_requests_bulk(self, pull_requests: List[PullRequest]) -> int:
pr.base_score,
pr.issue_multiplier,
pr.open_pr_spam_multiplier,
pr.repository_uniqueness_multiplier,
pr.pioneer_multiplier,
pr.pioneer_rank,
pr.time_decay_multiplier,
pr.credibility_multiplier,
pr.raw_credibility,
Expand Down
11 changes: 9 additions & 2 deletions tests/validator/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ def create(
repo: Optional[str] = None,
unique_repo: bool = False,
token_score: Optional[float] = None, # Auto-calculated from tier if None
merged_at: Optional[datetime] = None,
uid: int = 0,
) -> PullRequest:
"""Create a mock PullRequest with the given parameters.

Expand All @@ -96,6 +98,8 @@ def create(
If False and repo is None, uses 'test/repo'.
token_score: Token score for this PR. If None, auto-calculates based on tier
requirements to ensure the PR qualifies.
merged_at: Explicit merge timestamp. If None and state is MERGED, uses now().
uid: Miner UID for this PR (default 0).
"""
# Auto-calculate token score if not specified - ensure it meets tier requirements
if token_score is None:
Expand All @@ -110,15 +114,18 @@ def create(
if repo is None:
repo = self._next_repo() if unique_repo else 'test/repo'

if merged_at is None and state == PRState.MERGED:
merged_at = datetime.now(timezone.utc)

return PullRequest(
number=number,
repository_full_name=repo,
uid=0,
uid=uid,
hotkey='test_hotkey',
github_id='12345',
title=f'Test PR #{number}',
author_login='testuser',
merged_at=datetime.now(timezone.utc) if state == PRState.MERGED else None,
merged_at=merged_at,
created_at=datetime.now(timezone.utc),
pr_state=state,
repository_tier_configuration=tier,
Expand Down
Loading