lockdrop-simulation/lockdrop_calculations.py

1035 lines
48 KiB
Python

"""
Shared lockdrop calculation functions and utilities.
This module contains the reference calculation logic for lockdrop token distribution,
penalty systems, bonus pools, and final allocations. It can be used by both
the simulation notebook and the experimental notebook.
"""
from decimal import Decimal, ROUND_DOWN, getcontext
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import json
import os
from collections import defaultdict
from tabulate import tabulate
# Configure decimal precision
getcontext().prec = 28
getcontext().rounding = ROUND_DOWN
# Constants
TOKEN_PRECISION = Decimal('0.00000001') # 8 decimal precision
TOTAL_SUPPLY = 4294967296 # 1 $Z per Urbit ID
LOCKDROP_ALLOCATION_PERCENT = Decimal('0.3')
LOCKDROP_ALLOCATION = TOTAL_SUPPLY * LOCKDROP_ALLOCATION_PERCENT
# Points
NUM_GALAXIES = pow(2, 8)
NUM_STARS = pow(2, 16) - pow(2, 8)
NUM_PLANETS = pow(2, 32) - pow(2, 16)
# Allocation distribution
STAR_ALLOCATION_PERCENT = Decimal(NUM_STARS / pow(2, 16))
GALAXY_ALLOCATION_PERCENT = 1 - STAR_ALLOCATION_PERCENT
# Lockdrop duration (5 years including 1 leap year)
LOCKDROP_DURATION_SECONDS = 5 * 365.25 * 24 * 60 * 60
BLOCK_DURATION_SECONDS = 2
LOCKDROP_DURATION_BLOCKS = int(LOCKDROP_DURATION_SECONDS / BLOCK_DURATION_SECONDS)
# Penalty rates
PENALTY_RATES = {
5: Decimal('0'),
4: Decimal('0.2'),
3: Decimal('0.4'),
2: Decimal('0.6'),
1: Decimal('0.8')
}
def print_table_with_borders(df, title=""):
"""Print DataFrame with borders using tabulate."""
if title:
print(f"\n{title}")
print(tabulate(df, headers='keys', tablefmt='grid', showindex=False))
def configure_matplotlib():
"""Configure matplotlib settings for consistent plots."""
plt.style.use('seaborn-v0_8')
sns.set_palette("husl")
plt.rcParams['figure.figsize'] = (12, 8)
plt.rcParams['font.size'] = 11
plt.rcParams['axes.titlesize'] = 14
plt.rcParams['axes.labelsize'] = 12
plt.rcParams['xtick.labelsize'] = 10
plt.rcParams['ytick.labelsize'] = 10
plt.rcParams['legend.fontsize'] = 10
plt.rcParams['axes.unicode_minus'] = False
try:
plt.rcParams['font.family'] = 'DejaVu Sans'
except:
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'Liberation Sans', 'Arial', 'Helvetica']
def print_constants_summary():
"""Display core constants as formatted tables."""
print("=" * 80)
print("📊 $Z LOCKDROP DISTRIBUTION - CORE CONSTANTS")
print("=" * 80)
# Lockdrop Allocation Table
lockdrop_df = pd.DataFrame({
'Parameter': ['Total Supply (1 $Z per Urbit ID)', 'Lockdrop Allocation %', 'Lockdrop Allocation ($Z)'],
'Value': [f"{TOTAL_SUPPLY:,}", f"{LOCKDROP_ALLOCATION_PERCENT:.1%}", f"{LOCKDROP_ALLOCATION:,.1f}"]
})
print_table_with_borders(lockdrop_df, "🔒 LOCKDROP ALLOCATION")
# Points Distribution Table
points_df = pd.DataFrame({
'Point Type': ['Galaxies', 'Stars', 'Planets'],
'Count': [f"{NUM_GALAXIES:,}", f"{NUM_STARS:,}", f"{NUM_PLANETS:,}"],
'Allocation %': ['0.39%', '99.61%', '0%']
})
print_table_with_borders(points_df, "⭐ URBIT POINTS DISTRIBUTION")
# Lockdrop Parameters Table
params_df = pd.DataFrame({
'Parameter': ['Block Duration', 'Max Point Lock Duration (5 yrs)', 'Total Blocks', 'Star Allocation %', 'Galaxy Allocation %'],
'Value': [f"{BLOCK_DURATION_SECONDS} seconds", f"{LOCKDROP_DURATION_SECONDS:,.0f} seconds",
f"{LOCKDROP_DURATION_BLOCKS:,}", f"{STAR_ALLOCATION_PERCENT:.6%}", f"{GALAXY_ALLOCATION_PERCENT:.6%}"]
})
print_table_with_borders(params_df, "⏱️ LOCKDROP PARAMETERS")
print("\n" + "="*80)
def calculate_base_allocations():
"""Calculate base allocation amounts and quanta."""
lockdrop_allocation_stars = LOCKDROP_ALLOCATION * STAR_ALLOCATION_PERCENT
lockdrop_allocation_galaxies = LOCKDROP_ALLOCATION * GALAXY_ALLOCATION_PERCENT
assert (lockdrop_allocation_stars + lockdrop_allocation_galaxies) == LOCKDROP_ALLOCATION, "point allocation doesn't add up"
# Max allocation per point (theoretical)
max_allocation_per_star = lockdrop_allocation_stars / NUM_STARS
max_allocation_per_galaxy = lockdrop_allocation_galaxies / NUM_GALAXIES
# Quanta calculation
z_available_per_star_per_block = max_allocation_per_star / LOCKDROP_DURATION_BLOCKS
z_available_per_galaxy_per_block = max_allocation_per_galaxy / LOCKDROP_DURATION_BLOCKS
# Round down to 6 decimals
adjusted_z_per_star_per_block = z_available_per_star_per_block.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
adjusted_z_per_galaxy_per_block = z_available_per_galaxy_per_block.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
# Adjusted max allocation
adjusted_max_allocation_per_star = adjusted_z_per_star_per_block * LOCKDROP_DURATION_BLOCKS
adjusted_max_allocation_per_galaxy = adjusted_z_per_galaxy_per_block * LOCKDROP_DURATION_BLOCKS
return {
'lockdrop_allocation_stars': lockdrop_allocation_stars,
'lockdrop_allocation_galaxies': lockdrop_allocation_galaxies,
'max_allocation_per_star': max_allocation_per_star,
'max_allocation_per_galaxy': max_allocation_per_galaxy,
'adjusted_max_allocation_per_star': adjusted_max_allocation_per_star,
'adjusted_max_allocation_per_galaxy': adjusted_max_allocation_per_galaxy,
'adjusted_z_per_star_per_block': adjusted_z_per_star_per_block,
'adjusted_z_per_galaxy_per_block': adjusted_z_per_galaxy_per_block
}
def calculate_dynamic_allocations(participation_counts):
"""
Calculate allocations based on actual participation counts.
Args:
participation_counts: Dict with keys like 'stars_1_years', 'stars_2_years', etc.
"""
# Extract participation data
stars_counts = {year: Decimal(participation_counts[f'stars_{year}_years']) for year in range(1, 6)}
galaxies_counts = {year: Decimal(participation_counts[f'galaxies_{year}_years']) for year in range(1, 6)}
total_stars_locked = sum(stars_counts.values())
total_galaxies_locked = sum(galaxies_counts.values())
lockdrop_allocation_stars = LOCKDROP_ALLOCATION * STAR_ALLOCATION_PERCENT
lockdrop_allocation_galaxies = LOCKDROP_ALLOCATION * GALAXY_ALLOCATION_PERCENT
# Dynamic allocations based on actual participation
max_allocation_per_star = lockdrop_allocation_stars / total_stars_locked if total_stars_locked > 0 else Decimal('0')
max_allocation_per_galaxy = lockdrop_allocation_galaxies / total_galaxies_locked if total_galaxies_locked > 0 else Decimal('0')
# Quanta calculation
z_available_per_star_per_block = max_allocation_per_star / LOCKDROP_DURATION_BLOCKS
z_available_per_galaxy_per_block = max_allocation_per_galaxy / LOCKDROP_DURATION_BLOCKS
# Round down to 6 decimals
adjusted_z_per_star_per_block = z_available_per_star_per_block.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
adjusted_z_per_galaxy_per_block = z_available_per_galaxy_per_block.quantize(Decimal('0.000001'), rounding=ROUND_DOWN)
# Adjusted max allocation
adjusted_max_allocation_per_star = adjusted_z_per_star_per_block * LOCKDROP_DURATION_BLOCKS
adjusted_max_allocation_per_galaxy = adjusted_z_per_galaxy_per_block * LOCKDROP_DURATION_BLOCKS
# Rounding errors
rounding_error_per_star = max_allocation_per_star - adjusted_max_allocation_per_star
rounding_error_per_galaxy = max_allocation_per_galaxy - adjusted_max_allocation_per_galaxy
total_rounding_error_stars = lockdrop_allocation_stars - (adjusted_max_allocation_per_star * total_stars_locked)
total_rounding_error_galaxies = lockdrop_allocation_galaxies - (adjusted_max_allocation_per_galaxy * total_galaxies_locked)
return {
'stars_counts': stars_counts,
'galaxies_counts': galaxies_counts,
'total_stars_locked': total_stars_locked,
'total_galaxies_locked': total_galaxies_locked,
'adjusted_max_allocation_per_star': adjusted_max_allocation_per_star,
'adjusted_max_allocation_per_galaxy': adjusted_max_allocation_per_galaxy,
'total_rounding_error_stars': total_rounding_error_stars,
'total_rounding_error_galaxies': total_rounding_error_galaxies,
'adjusted_z_per_star_per_block': adjusted_z_per_star_per_block,
'adjusted_z_per_galaxy_per_block': adjusted_z_per_galaxy_per_block
}
def calculate_bonus_pools(allocation_data):
"""Calculate bonus pools from penalties and rounding errors."""
stars_counts = allocation_data['stars_counts']
galaxies_counts = allocation_data['galaxies_counts']
adjusted_max_allocation_per_star = allocation_data['adjusted_max_allocation_per_star']
adjusted_max_allocation_per_galaxy = allocation_data['adjusted_max_allocation_per_galaxy']
total_rounding_error_stars = allocation_data['total_rounding_error_stars']
total_rounding_error_galaxies = allocation_data['total_rounding_error_galaxies']
# Calculate penalty pools
star_penalty_pool = Decimal('0')
for years in [1, 2, 3, 4]:
penalty = PENALTY_RATES[years]
star_penalty_pool += adjusted_max_allocation_per_star * stars_counts[years] * penalty
galaxy_penalty_pool = Decimal('0')
for years in [1, 2, 3, 4]:
penalty = PENALTY_RATES[years]
galaxy_penalty_pool += adjusted_max_allocation_per_galaxy * galaxies_counts[years] * penalty
star_penalty_pool = star_penalty_pool.quantize(TOKEN_PRECISION, rounding=ROUND_DOWN)
galaxy_penalty_pool = galaxy_penalty_pool.quantize(TOKEN_PRECISION, rounding=ROUND_DOWN)
# Add rounding errors to bonus pools
star_bonus_pool_total = star_penalty_pool + total_rounding_error_stars
galaxy_bonus_pool_total = galaxy_penalty_pool + total_rounding_error_galaxies
# Calculate bonus per 5-year participant
stars_5_years = stars_counts[5]
galaxies_5_years = galaxies_counts[5]
bonus_per_star_5_years = star_bonus_pool_total / stars_5_years if stars_5_years > 0 else Decimal('0')
bonus_per_galaxy_5_years = galaxy_bonus_pool_total / galaxies_5_years if galaxies_5_years > 0 else Decimal('0')
return {
'star_penalty_pool': star_penalty_pool,
'galaxy_penalty_pool': galaxy_penalty_pool,
'star_bonus_pool_total': star_bonus_pool_total,
'galaxy_bonus_pool_total': galaxy_bonus_pool_total,
'bonus_per_star_5_years': bonus_per_star_5_years,
'bonus_per_galaxy_5_years': bonus_per_galaxy_5_years
}
def calculate_final_allocations(allocation_data, bonus_data):
"""Calculate final allocations for all lock periods."""
adjusted_max_allocation_per_star = allocation_data['adjusted_max_allocation_per_star']
adjusted_max_allocation_per_galaxy = allocation_data['adjusted_max_allocation_per_galaxy']
bonus_per_star_5_years = bonus_data['bonus_per_star_5_years']
bonus_per_galaxy_5_years = bonus_data['bonus_per_galaxy_5_years']
stars_counts = allocation_data['stars_counts']
galaxies_counts = allocation_data['galaxies_counts']
# Calculate final allocations
final_star_allocations = {}
final_galaxy_allocations = {}
for years in range(1, 6):
penalty = PENALTY_RATES[years]
if years == 5:
# 5-year participants get base + bonus
final_star_allocations[years] = (adjusted_max_allocation_per_star + bonus_per_star_5_years).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN)
final_galaxy_allocations[years] = (adjusted_max_allocation_per_galaxy + bonus_per_galaxy_5_years).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN)
else:
# Other years get penalized amounts
final_star_allocations[years] = (adjusted_max_allocation_per_star * (1 - penalty)).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN)
final_galaxy_allocations[years] = (adjusted_max_allocation_per_galaxy * (1 - penalty)).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN)
# Calculate Z per block for each lock period
star_z_per_block = {}
galaxy_z_per_block = {}
for years in range(1, 6):
lock_duration_fraction = Decimal(years) / Decimal('5') # Fraction of full 5-year period
effective_blocks = LOCKDROP_DURATION_BLOCKS * lock_duration_fraction
star_z_per_block[years] = (final_star_allocations[years] / effective_blocks).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN)
galaxy_z_per_block[years] = (final_galaxy_allocations[years] / effective_blocks).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN)
# Calculate total allocations for verification
total_stars_allocation = sum(final_star_allocations[year] * stars_counts[year] for year in range(1, 6))
total_galaxies_allocation = sum(final_galaxy_allocations[year] * galaxies_counts[year] for year in range(1, 6))
return {
'final_star_allocations': final_star_allocations,
'final_galaxy_allocations': final_galaxy_allocations,
'star_z_per_block': star_z_per_block,
'galaxy_z_per_block': galaxy_z_per_block,
'total_stars_allocation': total_stars_allocation,
'total_galaxies_allocation': total_galaxies_allocation
}
def generate_test_output(final_data):
"""Generate JSON output for test validation."""
final_star_allocations = final_data['final_star_allocations']
final_galaxy_allocations = final_data['final_galaxy_allocations']
total_allocation = final_data['total_stars_allocation'] + final_data['total_galaxies_allocation']
# Convert to $sZ units (multiply by 10^8)
output = {
"stars": {
f"{year}_years": int(final_star_allocations[year] * Decimal('1e8'))
for year in range(1, 6)
},
"galaxies": {
f"{year}_years": int(final_galaxy_allocations[year] * Decimal('1e8'))
for year in range(1, 6)
},
"total": int(total_allocation * Decimal('1e8'))
}
return output
def print_allocation_calculations(allocation_data):
"""Print basic allocation calculation tables."""
lockdrop_allocation_stars = LOCKDROP_ALLOCATION * STAR_ALLOCATION_PERCENT
lockdrop_allocation_galaxies = LOCKDROP_ALLOCATION * GALAXY_ALLOCATION_PERCENT
total_stars_locked = allocation_data['total_stars_locked']
total_galaxies_locked = allocation_data['total_galaxies_locked']
adjusted_max_allocation_per_star = allocation_data['adjusted_max_allocation_per_star']
adjusted_max_allocation_per_galaxy = allocation_data['adjusted_max_allocation_per_galaxy']
adjusted_z_per_star_per_block = allocation_data['adjusted_z_per_star_per_block']
adjusted_z_per_galaxy_per_block = allocation_data['adjusted_z_per_galaxy_per_block']
total_rounding_error_stars = allocation_data['total_rounding_error_stars']
total_rounding_error_galaxies = allocation_data['total_rounding_error_galaxies']
print("=" * 80)
print("🔢 ALLOCATION CALCULATIONS")
print("=" * 80)
# Raw Allocations Table
raw_allocation_per_star = lockdrop_allocation_stars / total_stars_locked if total_stars_locked > 0 else Decimal('0')
raw_allocation_per_galaxy = lockdrop_allocation_galaxies / total_galaxies_locked if total_galaxies_locked > 0 else Decimal('0')
raw_allocations_df = pd.DataFrame({
'Point Type': ['Stars', 'Galaxies'],
'Total Allocation ($Z)': [f"{lockdrop_allocation_stars:,.1f}", f"{lockdrop_allocation_galaxies:,.1f}"],
'Max Per Point ($Z)': [f"{raw_allocation_per_star:,.6f}", f"{raw_allocation_per_galaxy:,.6f}"]
})
print_table_with_borders(raw_allocations_df, "💰 RAW PARTICIPANT ALLOCATIONS")
# Quanta Calculations Table
raw_z_per_star_per_block = raw_allocation_per_star / LOCKDROP_DURATION_BLOCKS if total_stars_locked > 0 else Decimal('0')
raw_z_per_galaxy_per_block = raw_allocation_per_galaxy / LOCKDROP_DURATION_BLOCKS if total_galaxies_locked > 0 else Decimal('0')
quanta_df = pd.DataFrame({
'Point Type': ['Stars', 'Galaxies'],
'Raw Z per Block': [f"{raw_z_per_star_per_block:.15f}", f"{raw_z_per_galaxy_per_block:.15f}"],
'Adjusted Z per Block (q)': [f"{adjusted_z_per_star_per_block:.6f}", f"{adjusted_z_per_galaxy_per_block:.6f}"]
})
print_table_with_borders(quanta_df, "⚙️ QUANTA CALCULATION")
# Adjusted Allocations Table (after quanta calculation)
rounding_error_per_star = raw_allocation_per_star - adjusted_max_allocation_per_star
rounding_error_per_galaxy = raw_allocation_per_galaxy - adjusted_max_allocation_per_galaxy
percentage_rounding_error_stars = total_rounding_error_stars / TOTAL_SUPPLY * 100 if total_stars_locked > 0 else Decimal('0')
percentage_rounding_error_galaxies = total_rounding_error_galaxies / TOTAL_SUPPLY * 100 if total_galaxies_locked > 0 else Decimal('0')
# Separate tables for better readability
star_adjusted_df = pd.DataFrame({
'Metric': ['Adjusted Max per Star', 'Rounding Error per Star', 'Total Rounding Error', 'Rounding Error %'],
'Value': [f"{adjusted_max_allocation_per_star:,.6f} $Z", f"{rounding_error_per_star:.9f} $Z",
f"{total_rounding_error_stars:,.6f} $Z", f"{percentage_rounding_error_stars:.8f}%"],
'Note': ['Before bonus', 'Per star loss', 'Goes to bonus pool', 'Of total supply']
})
galaxy_adjusted_df = pd.DataFrame({
'Metric': ['Adjusted Max per Galaxy', 'Rounding Error per Galaxy', 'Total Rounding Error', 'Rounding Error %'],
'Value': [f"{adjusted_max_allocation_per_galaxy:,.6f} $Z", f"{rounding_error_per_galaxy:.9f} $Z",
f"{total_rounding_error_galaxies:,.6f} $Z", f"{percentage_rounding_error_galaxies:.8f}%"],
'Note': ['Before bonus', 'Per galaxy loss', 'Goes to bonus pool', 'Of total supply']
})
print("\n🎯 QUANTA ADJUSTED PARTICIPANT ALLOCATIONS")
print_table_with_borders(star_adjusted_df, "⭐ Star Adjustments")
print_table_with_borders(galaxy_adjusted_df, "🌌 Galaxy Adjustments")
print("\n" + "="*80)
def print_penalty_analysis(allocation_data):
"""Print penalty system analysis using values before bonus calculations."""
print("=" * 80)
print("⚖️ PENALTY SYSTEM ANALYSIS")
print("=" * 80)
# Use adjusted allocations (after penalties, before bonus distribution)
adjusted_max_allocation_per_star = allocation_data['adjusted_max_allocation_per_star']
adjusted_max_allocation_per_galaxy = allocation_data['adjusted_max_allocation_per_galaxy']
penalty_analysis_df = pd.DataFrame({
'Lock Period': ['5 Years', '4 Years', '3 Years', '2 Years', '1 Year'],
'Penalty Rate': [f"{PENALTY_RATES[year]:.1%}" for year in [5, 4, 3, 2, 1]],
'Star Allocation ($Z)': [f"{adjusted_max_allocation_per_star * (1 - PENALTY_RATES[year]):,.6f}" for year in [5, 4, 3, 2, 1]],
'Galaxy Allocation ($Z)': [f"{adjusted_max_allocation_per_galaxy * (1 - PENALTY_RATES[year]):,.6f}" for year in [5, 4, 3, 2, 1]],
'vs Max Allocation': ['100%', '80%', '60%', '40%', '20%']
})
print_table_with_borders(penalty_analysis_df, "📊 PENALTY ADJUSTED ALLOCATIONS (Before Bonus Distribution)")
print("\n" + "="*80)
def print_participation_summary(allocation_data):
"""Print participation distribution summary."""
stars_counts = allocation_data['stars_counts']
galaxies_counts = allocation_data['galaxies_counts']
total_stars_locked = allocation_data['total_stars_locked']
total_galaxies_locked = allocation_data['total_galaxies_locked']
# Consolidated participation and lock period distribution
participation_df = pd.DataFrame({
'Lock Period': ['1 Year', '2 Years', '3 Years', '4 Years', '5 Years', 'Total'],
'Stars': [f"{stars_counts[year]:,}" for year in [1, 2, 3, 4, 5]] + [f"{total_stars_locked:,}"],
'% of Total Stars': [f"{stars_counts[year]/NUM_STARS:.2%}" for year in [1, 2, 3, 4, 5]] + [f"{total_stars_locked/NUM_STARS:.1%}"],
'Galaxies': [f"{galaxies_counts[year]:,}" for year in [1, 2, 3, 4, 5]] + [f"{total_galaxies_locked:,}"],
'% of Total Galaxies': [f"{galaxies_counts[year]/NUM_GALAXIES:.2%}" for year in [1, 2, 3, 4, 5]] + [f"{total_galaxies_locked/NUM_GALAXIES:.1%}"],
'Total': [f"{stars_counts[year] + galaxies_counts[year]:,}" for year in [1, 2, 3, 4, 5]] + [f"{total_stars_locked + total_galaxies_locked:,}"],
})
print("=" * 80)
print_table_with_borders(participation_df, "🎯 PARTICIPANTS LOCK PERIOD DISTRIBUTION")
print("\n" + "="*80)
def print_bonus_pool_calculations(allocation_data, bonus_data):
"""Print detailed bonus pool calculations."""
stars_counts = allocation_data['stars_counts']
galaxies_counts = allocation_data['galaxies_counts']
adjusted_max_allocation_per_star = allocation_data['adjusted_max_allocation_per_star']
adjusted_max_allocation_per_galaxy = allocation_data['adjusted_max_allocation_per_galaxy']
total_rounding_error_stars = allocation_data['total_rounding_error_stars']
total_rounding_error_galaxies = allocation_data['total_rounding_error_galaxies']
print("=" * 80)
print("🎁 BONUS POOL CALCULATIONS")
print("=" * 80)
# Star Bonus Pool Analysis
star_bonus_df = pd.DataFrame({
'Component': ['Penalty Pool', 'Rounding Error Bonus', 'Total Star Bonus Pool',
'Recipients (5Y Stars)', 'Bonus per 5Y Star', 'Final 5Y Star Allocation'],
'Value': [f"{bonus_data['star_penalty_pool']:,.6f} $Z", f"{total_rounding_error_stars:,.6f} $Z",
f"{bonus_data['star_bonus_pool_total']:,.6f} $Z", f"{stars_counts[5]:,}",
f"{bonus_data['bonus_per_star_5_years']:,.6f} $Z", f"{adjusted_max_allocation_per_star + bonus_data['bonus_per_star_5_years']:,.6f} $Z"]
})
print_table_with_borders(star_bonus_df, "⭐ STAR BONUS POOL ANALYSIS")
# Galaxy Bonus Pool Analysis
galaxy_bonus_df = pd.DataFrame({
'Component': ['Penalty Pool', 'Rounding Error Bonus', 'Total Galaxy Bonus Pool',
'Recipients (5Y Galaxies)', 'Bonus per 5Y Galaxy', 'Final 5Y Galaxy Allocation'],
'Value': [f"{bonus_data['galaxy_penalty_pool']:,.6f} $Z", f"{total_rounding_error_galaxies:,.6f} $Z",
f"{bonus_data['galaxy_bonus_pool_total']:,.6f} $Z", f"{galaxies_counts[5]:,}",
f"{bonus_data['bonus_per_galaxy_5_years']:,.6f} $Z", f"{adjusted_max_allocation_per_galaxy + bonus_data['bonus_per_galaxy_5_years']:,.6f} $Z"]
})
print_table_with_borders(galaxy_bonus_df, "🌌 GALAXY BONUS POOL ANALYSIS")
print("\n" + "="*80)
def print_final_allocations_and_verification(allocation_data, final_data):
"""Print final allocations and verification tables."""
stars_counts = allocation_data['stars_counts']
galaxies_counts = allocation_data['galaxies_counts']
final_star_allocations = final_data['final_star_allocations']
final_galaxy_allocations = final_data['final_galaxy_allocations']
star_z_per_block = final_data['star_z_per_block']
galaxy_z_per_block = final_data['galaxy_z_per_block']
total_stars_allocation = final_data['total_stars_allocation']
total_galaxies_allocation = final_data['total_galaxies_allocation']
print("=" * 80)
print("✅ FINAL ALLOCATIONS & VERIFICATION")
print("=" * 80)
# Star Final Allocations Table
star_final_df = pd.DataFrame({
'Lock Period': ['5 Years', '4 Years', '3 Years', '2 Years', '1 Year'],
'Penalty': [f"{PENALTY_RATES[year]:.1%}" for year in [5, 4, 3, 2, 1]],
'Final Allocation ($Z)': [f"{final_star_allocations[year]:,.8f}" for year in [5, 4, 3, 2, 1]],
'Z per Block': [f"{star_z_per_block[year]:,.8f}" for year in [5, 4, 3, 2, 1]],
'Participants': [f"{stars_counts[year]:,}" for year in [5, 4, 3, 2, 1]]
})
print_table_with_borders(star_final_df, "⭐ FINAL STAR ALLOCATIONS")
# Galaxy Final Allocations Table
galaxy_final_df = pd.DataFrame({
'Lock Period': ['5 Years', '4 Years', '3 Years', '2 Years', '1 Year'],
'Penalty': [f"{PENALTY_RATES[year]:.1%}" for year in [5, 4, 3, 2, 1]],
'Final Allocation ($Z)': [f"{final_galaxy_allocations[year]:,.8f}" for year in [5, 4, 3, 2, 1]],
'Z per Block': [f"{galaxy_z_per_block[year]:,.8f}" for year in [5, 4, 3, 2, 1]],
'Participants': [f"{galaxies_counts[year]:,}" for year in [5, 4, 3, 2, 1]]
})
print_table_with_borders(galaxy_final_df, "🌌 FINAL GALAXY ALLOCATIONS")
# Verification and Rounding Error Analysis
lockdrop_allocation_stars = LOCKDROP_ALLOCATION * STAR_ALLOCATION_PERCENT
lockdrop_allocation_galaxies = LOCKDROP_ALLOCATION * GALAXY_ALLOCATION_PERCENT
final_rounding_error_stars = lockdrop_allocation_stars - total_stars_allocation
final_rounding_error_galaxies = lockdrop_allocation_galaxies - total_galaxies_allocation
final_rounding_error = final_rounding_error_stars + final_rounding_error_galaxies
verification_df = pd.DataFrame({
'Category': ['Star Allocations', 'Galaxy Allocations', 'Combined'],
'Calculated Total': [f"{total_stars_allocation:.8f} $Z", f"{total_galaxies_allocation:.8f} $Z",
f"{total_stars_allocation + total_galaxies_allocation:.8f} $Z"],
'Expected Total': [f"{lockdrop_allocation_stars:.8f} $Z", f"{lockdrop_allocation_galaxies:.8f} $Z",
f"{LOCKDROP_ALLOCATION:.8f} $Z"],
'Rounding Error': [f"{final_rounding_error_stars:.8f} $Z", f"{final_rounding_error_galaxies:.8f} $Z",
f"{final_rounding_error:.8f} $Z"]
})
print_table_with_borders(verification_df, "🔍 ALLOCATION VERIFICATION")
print("\n📝 NOTE: Final rounding errors go to Zenith Foundation")
print("\n" + "="*80)
def print_analysis_tables(allocation_data, bonus_data, final_data):
"""Print comprehensive analysis tables - calls all the detailed sections."""
print_participation_summary(allocation_data)
print_allocation_calculations(allocation_data)
print_penalty_analysis(allocation_data)
print_bonus_pool_calculations(allocation_data, bonus_data)
print_final_allocations_and_verification(allocation_data, final_data)
def create_visualization(allocation_data, final_data):
"""Create comprehensive visualization plots."""
stars_counts = allocation_data['stars_counts']
galaxies_counts = allocation_data['galaxies_counts']
final_star_allocations = final_data['final_star_allocations']
final_galaxy_allocations = final_data['final_galaxy_allocations']
fig = plt.figure(figsize=(20, 10))
gs = fig.add_gridspec(2, 3, hspace=0.35, wspace=0.35)
fig.suptitle('Lockdrop Analysis', fontsize=18, fontweight='bold', y=0.98)
# 1. Star Participation Distribution
ax1 = fig.add_subplot(gs[0, 0])
star_years = [1, 2, 3, 4, 5]
star_counts = [stars_counts[year] for year in star_years]
bars1 = ax1.bar(range(len(star_years)), star_counts,
color=['#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728'], alpha=0.8)
ax1.set_xlabel('Lock Period (Years)')
ax1.set_ylabel('Number of Stars')
ax1.set_title('Star Participation by Lock Period', fontweight='bold')
ax1.set_xticks(range(len(star_years)))
ax1.set_xticklabels(star_years)
for i, count in enumerate(star_counts):
height = bars1[i].get_height()
ax1.text(bars1[i].get_x() + bars1[i].get_width()/2., height + height*0.01,
f'{count:,}', ha='center', va='bottom', fontweight='bold', fontsize=9)
# 2. Galaxy Participation Distribution
ax2 = fig.add_subplot(gs[0, 1])
galaxy_counts = [galaxies_counts[year] for year in star_years]
bars2 = ax2.bar(range(len(star_years)), galaxy_counts,
color=['#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2'], alpha=0.8)
ax2.set_xlabel('Lock Period (Years)')
ax2.set_ylabel('Number of Galaxies')
ax2.set_title('Galaxy Participation by Lock Period', fontweight='bold')
ax2.set_xticks(range(len(star_years)))
ax2.set_xticklabels(star_years)
for i, count in enumerate(galaxy_counts):
height = bars2[i].get_height()
ax2.text(bars2[i].get_x() + bars2[i].get_width()/2., height + height*0.01,
f'{count:,}', ha='center', va='bottom', fontweight='bold', fontsize=9)
# 3. Star Allocations
ax3 = fig.add_subplot(gs[0, 2])
star_alloc_values = [float(final_star_allocations[year]) for year in star_years]
bars3 = ax3.bar(range(len(star_years)), star_alloc_values,
color=['#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728'], alpha=0.8)
ax3.set_xlabel('Lock Period (Years)')
ax3.set_ylabel('Allocation per Star ($Z)')
ax3.set_title('Star Allocations by Lock Period', fontweight='bold')
ax3.set_xticks(range(len(star_years)))
ax3.set_xticklabels(star_years)
for i, allocation in enumerate(star_alloc_values):
height = bars3[i].get_height()
ax3.text(bars3[i].get_x() + bars3[i].get_width()/2., height + height*0.01,
f'{allocation:,.0f}', ha='center', va='bottom', fontweight='bold', fontsize=9)
# 4. Galaxy Allocations
ax4 = fig.add_subplot(gs[1, 0])
galaxy_alloc_values = [float(final_galaxy_allocations[year]) for year in star_years]
bars4 = ax4.bar(range(len(star_years)), galaxy_alloc_values,
color=['#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2'], alpha=0.8)
ax4.set_xlabel('Lock Period (Years)')
ax4.set_ylabel('Allocation per Galaxy ($Z)')
ax4.set_title('Galaxy Allocations by Lock Period', fontweight='bold')
ax4.set_xticks(range(len(star_years)))
ax4.set_xticklabels(star_years)
for i, allocation in enumerate(galaxy_alloc_values):
height = bars4[i].get_height()
ax4.text(bars4[i].get_x() + bars4[i].get_width()/2., height + height*0.01,
f'{allocation:,.0f}', ha='center', va='bottom', fontweight='bold', fontsize=9)
# Hide remaining subplots
ax5 = fig.add_subplot(gs[1, 1])
ax5.axis('off')
ax6 = fig.add_subplot(gs[1, 2])
ax6.axis('off')
plt.subplots_adjust(bottom=0.08, top=0.88, left=0.05, right=0.98)
plt.show()
def load_watcher_events_data(generated_dir='generated'):
"""Load participation data from watcher events file."""
import urbitob
watcher_events_path = os.path.join(generated_dir, 'watcher-events.json')
try:
with open(watcher_events_path, 'r') as f:
data = json.load(f)
events = data['data']['eventsInRange']
# Analyze events
lock_duration_counts = {
'star': defaultdict(int),
'galaxy': defaultdict(int)
}
for event_data in events:
if event_data['event']['__typename'] == 'PointLockedEvent':
point = event_data['event']['point']
lock_period = event_data['event']['lock_period']
point_num = urbitob.patp_to_num(point)
point_type = "galaxy" if point_num < NUM_GALAXIES else "star"
lock_duration_counts[point_type][lock_period] += 1
# Extract counts
result = {}
for years in [1, 2, 3, 4, 5]:
result[f'stars_{years}_years'] = lock_duration_counts['star'][years]
result[f'galaxies_{years}_years'] = lock_duration_counts['galaxy'][years]
return result, True
except FileNotFoundError:
print(f"⚠️ Watcher events file not found at {watcher_events_path}")
return None, False
except Exception as e:
print(f"⚠️ Error loading watcher events: {e}")
return None, False
# Experimental notebook widget and interaction functions
def create_experiment_widgets():
"""Create input widgets for the experimental notebook."""
try:
import ipywidgets as widgets
except ImportError:
raise ImportError("ipywidgets is required for the experimental interface. Install with: pip install ipywidgets")
# Define reasonable default values
default_participants = {
'stars_1_years': 8000,
'stars_2_years': 8000,
'stars_3_years': 8000,
'stars_4_years': 8000,
'stars_5_years': 8000,
'galaxies_1_years': 40,
'galaxies_2_years': 40,
'galaxies_3_years': 40,
'galaxies_4_years': 40,
'galaxies_5_years': 40
}
# Create input widgets
print("="*50)
# Star participation widgets
stars_1_years = widgets.IntText(value=default_participants['stars_1_years'], description='1 Year:', style={'description_width': '80px'})
stars_2_years = widgets.IntText(value=default_participants['stars_2_years'], description='2 Years:', style={'description_width': '80px'})
stars_3_years = widgets.IntText(value=default_participants['stars_3_years'], description='3 Years:', style={'description_width': '80px'})
stars_4_years = widgets.IntText(value=default_participants['stars_4_years'], description='4 Years:', style={'description_width': '80px'})
stars_5_years = widgets.IntText(value=default_participants['stars_5_years'], description='5 Years:', style={'description_width': '80px'})
star_controls = widgets.VBox([stars_1_years, stars_2_years, stars_3_years, stars_4_years, stars_5_years])
galaxies_1_years = widgets.IntText(value=default_participants['galaxies_1_years'], description='1 Year:', style={'description_width': '80px'})
galaxies_2_years = widgets.IntText(value=default_participants['galaxies_2_years'], description='2 Years:', style={'description_width': '80px'})
galaxies_3_years = widgets.IntText(value=default_participants['galaxies_3_years'], description='3 Years:', style={'description_width': '80px'})
galaxies_4_years = widgets.IntText(value=default_participants['galaxies_4_years'], description='4 Years:', style={'description_width': '80px'})
galaxies_5_years = widgets.IntText(value=default_participants['galaxies_5_years'], description='5 Years:', style={'description_width': '80px'})
galaxy_controls = widgets.VBox([galaxies_1_years, galaxies_2_years, galaxies_3_years, galaxies_4_years, galaxies_5_years])
# Preset scenario dropdown
scenario_dropdown = widgets.Dropdown(
options=[('Select Preset', '')] + [(v['description'], k) for k, v in SCENARIOS.items()],
description='Preset:',
style={'description_width': '60px'},
layout={'width': '400px'}
)
# Calculate button
calculate_button = widgets.Button(description='🔄 Calculate Allocations', button_style='success', layout={'width': '200px', 'margin': '10px 5px'})
# Export button
export_button = widgets.Button(description='💾 Export Results', button_style='info', layout={'width': '180px', 'margin': '10px 5px'})
# Output area
output_area = widgets.Output()
# Set up preset selection handler
def on_preset_change(change):
if change['new'] and change['new'] != '':
scenario = SCENARIOS[change['new']]
params = scenario['params']
# Update widget values
stars_1_years.value = params['stars_1_years']
stars_2_years.value = params['stars_2_years']
stars_3_years.value = params['stars_3_years']
stars_4_years.value = params['stars_4_years']
stars_5_years.value = params['stars_5_years']
galaxies_1_years.value = params['galaxies_1_years']
galaxies_2_years.value = params['galaxies_2_years']
galaxies_3_years.value = params['galaxies_3_years']
galaxies_4_years.value = params['galaxies_4_years']
galaxies_5_years.value = params['galaxies_5_years']
scenario_dropdown.observe(on_preset_change, names='value')
# Set up export button handler
def on_export_click(button=None):
"""Handle export button click with timestamp."""
try:
from datetime import datetime
from IPython.display import clear_output
except ImportError:
print("❌ Required libraries not available")
return
# Generate timestamp for filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f'lockdrop_calculations_result_{timestamp}.json'
# Get current values from widgets
participation_counts = {
'stars_1_years': stars_1_years.value,
'stars_2_years': stars_2_years.value,
'stars_3_years': stars_3_years.value,
'stars_4_years': stars_4_years.value,
'stars_5_years': stars_5_years.value,
'galaxies_1_years': galaxies_1_years.value,
'galaxies_2_years': galaxies_2_years.value,
'galaxies_3_years': galaxies_3_years.value,
'galaxies_4_years': galaxies_4_years.value,
'galaxies_5_years': galaxies_5_years.value
}
try:
# Calculate allocations
allocation_data = calculate_dynamic_allocations(participation_counts)
bonus_data = calculate_bonus_pools(allocation_data)
final_data = calculate_final_allocations(allocation_data, bonus_data)
# Generate test output
test_output = generate_test_output(final_data)
# Add metadata
export_data = {
'metadata': {
'source': 'lockdrop-calculations.ipynb',
'timestamp': timestamp,
'participation_counts': participation_counts
},
'allocations': test_output
}
# Save to file
with open(filename, 'w') as f:
json.dump(export_data, f, indent=2)
print(f"✅ Results exported to: {filename}")
except Exception as e:
print(f"❌ Export failed: {str(e)}")
export_button.on_click(on_export_click)
# Return all widgets as a dictionary
return {
'star_controls': star_controls,
'galaxy_controls': galaxy_controls,
'scenario_dropdown': scenario_dropdown,
'calculate_button': calculate_button,
'export_button': export_button,
'output_area': output_area,
'widgets': {
'stars_1_years': stars_1_years,
'stars_2_years': stars_2_years,
'stars_3_years': stars_3_years,
'stars_4_years': stars_4_years,
'stars_5_years': stars_5_years,
'galaxies_1_years': galaxies_1_years,
'galaxies_2_years': galaxies_2_years,
'galaxies_3_years': galaxies_3_years,
'galaxies_4_years': galaxies_4_years,
'galaxies_5_years': galaxies_5_years
}
}
def create_calculate_function(widget_dict):
"""Create the calculation and display function for the experimental notebook."""
def calculate_and_display(button=None):
"""Calculate allocations and display results based on input parameters."""
try:
from IPython.display import clear_output
except ImportError:
raise ImportError("IPython is required for the experimental interface")
widgets_map = widget_dict['widgets']
output_area = widget_dict['output_area']
with output_area:
clear_output(wait=True)
# Get current values from widgets
participation_counts = {
'stars_1_years': widgets_map['stars_1_years'].value,
'stars_2_years': widgets_map['stars_2_years'].value,
'stars_3_years': widgets_map['stars_3_years'].value,
'stars_4_years': widgets_map['stars_4_years'].value,
'stars_5_years': widgets_map['stars_5_years'].value,
'galaxies_1_years': widgets_map['galaxies_1_years'].value,
'galaxies_2_years': widgets_map['galaxies_2_years'].value,
'galaxies_3_years': widgets_map['galaxies_3_years'].value,
'galaxies_4_years': widgets_map['galaxies_4_years'].value,
'galaxies_5_years': widgets_map['galaxies_5_years'].value
}
# Validate inputs
total_stars = sum(participation_counts[key] for key in participation_counts if 'stars' in key)
total_galaxies = sum(participation_counts[key] for key in participation_counts if 'galaxies' in key)
if total_stars > NUM_STARS:
print(f"⚠️ ERROR: Total stars ({total_stars:,}) exceeds maximum available ({NUM_STARS:,})")
return
if total_galaxies > NUM_GALAXIES:
print(f"⚠️ ERROR: Total galaxies ({total_galaxies:,}) exceeds maximum available ({NUM_GALAXIES:,})")
return
if total_stars == 0 and total_galaxies == 0:
print("⚠️ ERROR: Must have at least some participants")
return
try:
# Perform calculations
print("🔄 Calculating allocations...\n")
allocation_data = calculate_dynamic_allocations(participation_counts)
bonus_data = calculate_bonus_pools(allocation_data)
final_data = calculate_final_allocations(allocation_data, bonus_data)
# Display results
print_analysis_tables(allocation_data, bonus_data, final_data)
# Create visualization
print("\n📊 Generating visualization...")
create_visualization(allocation_data, final_data)
# Calculate participation insights
star_participation_rate = Decimal(total_stars) / NUM_STARS
galaxy_participation_rate = Decimal(total_galaxies) / NUM_GALAXIES
print("\n" + "="*80)
print("🎯 EXPERIMENT INSIGHTS")
print("=" * 80)
insights_df = pd.DataFrame({
'Metric': [
'Total Participants',
'Star Participation Rate',
'Galaxy Participation Rate',
'5-Year Star Bonus',
'5-Year Galaxy Bonus',
'Highest Individual Allocation',
'Lowest Individual Allocation'
],
'Value': [
f"{total_stars + total_galaxies:,}",
f"{star_participation_rate:.1%}",
f"{galaxy_participation_rate:.1%}",
f"{float(bonus_data['bonus_per_star_5_years']):,.2f} $Z" if participation_counts['stars_5_years'] > 0 else "N/A (no 5Y stars)",
f"{float(bonus_data['bonus_per_galaxy_5_years']):,.2f} $Z" if participation_counts['galaxies_5_years'] > 0 else "N/A (no 5Y galaxies)",
f"{max(float(final_data['final_star_allocations'][5]), float(final_data['final_galaxy_allocations'][5])):,.2f} $Z",
f"{min(float(final_data['final_star_allocations'][1]), float(final_data['final_galaxy_allocations'][1])):,.2f} $Z"
]
})
print_table_with_borders(insights_df, "🎯 EXPERIMENT INSIGHTS")
print("\n" + "="*80)
except Exception as e:
print(f"❌ Error during calculation: {str(e)}")
import traceback
traceback.print_exc()
return calculate_and_display
# Preset scenario definitions for experimentation
SCENARIOS = {
'balanced': {
'description': 'Balanced distribution across all lock periods',
'params': {
'stars_1_years': 8000, 'stars_2_years': 8000, 'stars_3_years': 8000, 'stars_4_years': 8000, 'stars_5_years': 8000,
'galaxies_1_years': 40, 'galaxies_2_years': 40, 'galaxies_3_years': 40, 'galaxies_4_years': 40, 'galaxies_5_years': 40
}
},
'five_year_focused': {
'description': 'Most participants choose 5-year lock (maximum bonus scenario)',
'params': {
'stars_1_years': 2000, 'stars_2_years': 2000, 'stars_3_years': 3000, 'stars_4_years': 5000, 'stars_5_years': 28000,
'galaxies_1_years': 10, 'galaxies_2_years': 10, 'galaxies_3_years': 20, 'galaxies_4_years': 30, 'galaxies_5_years': 130
}
},
'short_term_focused': {
'description': 'Most participants choose shorter locks (high penalty scenario)',
'params': {
'stars_1_years': 20000, 'stars_2_years': 15000, 'stars_3_years': 4000, 'stars_4_years': 800, 'stars_5_years': 200,
'galaxies_1_years': 100, 'galaxies_2_years': 80, 'galaxies_3_years': 15, 'galaxies_4_years': 3, 'galaxies_5_years': 2
}
},
'low_participation': {
'description': 'Low overall participation scenario',
'params': {
'stars_1_years': 1000, 'stars_2_years': 800, 'stars_3_years': 600, 'stars_4_years': 400, 'stars_5_years': 200,
'galaxies_1_years': 8, 'galaxies_2_years': 6, 'galaxies_3_years': 4, 'galaxies_4_years': 2, 'galaxies_5_years': 5
}
}
}
def create_scenario_loader(widget_dict):
"""Create scenario loading function for the experimental notebook."""
def load_scenario(scenario_name):
"""Load a preset scenario into the input widgets."""
if scenario_name not in SCENARIOS:
print(f"❌ Unknown scenario: {scenario_name}")
return
scenario = SCENARIOS[scenario_name]
params = scenario['params']
widgets_map = widget_dict['widgets']
# Update widget values
widgets_map['stars_1_years'].value = params['stars_1_years']
widgets_map['stars_2_years'].value = params['stars_2_years']
widgets_map['stars_3_years'].value = params['stars_3_years']
widgets_map['stars_4_years'].value = params['stars_4_years']
widgets_map['stars_5_years'].value = params['stars_5_years']
widgets_map['galaxies_1_years'].value = params['galaxies_1_years']
widgets_map['galaxies_2_years'].value = params['galaxies_2_years']
widgets_map['galaxies_3_years'].value = params['galaxies_3_years']
widgets_map['galaxies_4_years'].value = params['galaxies_4_years']
widgets_map['galaxies_5_years'].value = params['galaxies_5_years']
print(f"✅ Loaded scenario: {scenario_name}")
print(f"📝 Description: {scenario['description']}")
print("\n🔄 Click 'Calculate Allocations' button above to see results!")
return load_scenario
def create_export_function(widget_dict):
"""Create export function for the experimental notebook."""
def export_current_scenario(filename='lockdrop_allocations_experiment.json'):
"""Export current scenario results to JSON file."""
widgets_map = widget_dict['widgets']
# Get current values from widgets
participation_counts = {
'stars_1_years': widgets_map['stars_1_years'].value,
'stars_2_years': widgets_map['stars_2_years'].value,
'stars_3_years': widgets_map['stars_3_years'].value,
'stars_4_years': widgets_map['stars_4_years'].value,
'stars_5_years': widgets_map['stars_5_years'].value,
'galaxies_1_years': widgets_map['galaxies_1_years'].value,
'galaxies_2_years': widgets_map['galaxies_2_years'].value,
'galaxies_3_years': widgets_map['galaxies_3_years'].value,
'galaxies_4_years': widgets_map['galaxies_4_years'].value,
'galaxies_5_years': widgets_map['galaxies_5_years'].value
}
try:
# Calculate allocations
allocation_data = calculate_dynamic_allocations(participation_counts)
bonus_data = calculate_bonus_pools(allocation_data)
final_data = calculate_final_allocations(allocation_data, bonus_data)
# Generate test output
test_output = generate_test_output(final_data)
# Add metadata
export_data = {
'metadata': {
'source': 'lockdrop-calculations.ipynb',
'participation_counts': participation_counts
},
'allocations': test_output
}
# Save to file
with open(filename, 'w') as f:
json.dump(export_data, f, indent=2)
print(f"✅ Results exported to: {filename}")
print(f"📊 Total participants: {sum(participation_counts.values()):,}")
except Exception as e:
print(f"❌ Export failed: {str(e)}")
return export_current_scenario