diff --git a/EXPERIMENT.md b/EXPERIMENT.md index d352b44..e5f9952 100644 --- a/EXPERIMENT.md +++ b/EXPERIMENT.md @@ -30,7 +30,7 @@ This guide explains how to use the interactive notebook to experiment with diffe 3. **Use the Interactive Interface** - The notebook will open automatically in your browser. Execute the cells in order to start experimenting with different participation scenarios. + The notebook will open automatically in your browser. Execute it by clicking "Run All" from `Run` tab in order to start experimenting with different participation scenarios. ## Using the Interactive Interface diff --git a/lockdrop-calculations-simulated.ipynb b/lockdrop-calculations-simulated.ipynb index af72dd5..174b6b4 100644 --- a/lockdrop-calculations-simulated.ipynb +++ b/lockdrop-calculations-simulated.ipynb @@ -5,7 +5,7 @@ "id": "09975f67", "metadata": {}, "source": [ - "## Z Token Lockdrop Distribution" + "## Z Token Lockdrop Distribution (Simulation)" ] }, { @@ -23,7 +23,7 @@ "outputs": [], "source": [ "# Import shared calculation module\n", - "from lockdrop_calculations import (\n", + "from lockdrop import (\n", " configure_matplotlib, print_constants_summary,\n", " run_simulation_analysis, create_visualization\n", ")\n", diff --git a/lockdrop-calculations.ipynb b/lockdrop-calculations.ipynb index 50bbb28..6ceff8f 100644 --- a/lockdrop-calculations.ipynb +++ b/lockdrop-calculations.ipynb @@ -24,7 +24,7 @@ "outputs": [], "source": [ "# Import shared calculation module\n", - "from lockdrop_calculations import (\n", + "from lockdrop import (\n", " configure_matplotlib, print_constants_summary,\n", " create_experimental_interface\n", ")\n", @@ -64,14 +64,6 @@ "# Create complete experimental interface\n", "create_experimental_interface()" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17264a50-2d4b-4c83-aa08-c3d2e1d90fdd", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/lockdrop/__init__.py b/lockdrop/__init__.py new file mode 100644 index 0000000..ef71743 --- /dev/null +++ b/lockdrop/__init__.py @@ -0,0 +1,25 @@ +""" +Lockdrop calculations module. + +This package provides a modular implementation of lockdrop token distribution calculations, +penalty systems, bonus pools, and interactive experimental interfaces. +""" + +# Import all public functions for backward compatibility +from .constants import * +from .calculations import * +from .display import ( + print_constants_summary, + print_analysis_tables +) +from .visualization import * +from .widgets import ( + create_experimental_interface +) +from .simulation import ( + run_simulation_analysis +) + +__version__ = "1.0.0" + +# TODO: Use this package in zenithd notebooks diff --git a/lockdrop/calculations.py b/lockdrop/calculations.py new file mode 100644 index 0000000..fdfec26 --- /dev/null +++ b/lockdrop/calculations.py @@ -0,0 +1,185 @@ +""" +Core lockdrop calculation functions. + +This module contains the main mathematical functions for calculating token allocations, +penalties, bonuses, and final distributions. +""" + +from decimal import Decimal, ROUND_DOWN +from .constants import ( + TOKEN_PRECISION, LOCKDROP_ALLOCATION, STAR_ALLOCATION_PERCENT, + GALAXY_ALLOCATION_PERCENT, + LOCKDROP_DURATION_BLOCKS, PENALTY_RATES +) + + +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, + 'rounding_error_per_star': rounding_error_per_star, + 'rounding_error_per_galaxy': rounding_error_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_allocation_sz": int(total_allocation * Decimal('1e8')) + } + + return output diff --git a/lockdrop/constants.py b/lockdrop/constants.py new file mode 100644 index 0000000..8503888 --- /dev/null +++ b/lockdrop/constants.py @@ -0,0 +1,64 @@ +""" +Lockdrop calculation constants and configuration. + +This module contains all constants used in lockdrop token distribution calculations. +""" + +from decimal import Decimal, ROUND_DOWN, getcontext + +# Configure decimal precision +getcontext().prec = 28 +getcontext().rounding = ROUND_DOWN + +# Token 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 + +# Urbit Point Counts +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 by Lock Period +PENALTY_RATES = { + 5: Decimal('0'), + 4: Decimal('0.2'), + 3: Decimal('0.4'), + 2: Decimal('0.6'), + 1: Decimal('0.8') +} + +# Preset Scenarios for Experimentation +SCENARIOS = { + "balanced": { + "description": 'Balanced distribution across all lock periods', + "stars": {"1_year": 8000, "2_year": 8000, "3_year": 8000, "4_year": 8000, "5_year": 8000}, + "galaxies": {"1_year": 40, "2_year": 40, "3_year": 40, "4_year": 40, "5_year": 40} + }, + "five_year_focused": { + "description": 'Most participants choose 5-year lock (maximum bonus scenario)', + "stars": {"1_year": 2000, "2_year": 2000, "3_year": 3000, "4_year": 5000, "5_year": 28000}, + "galaxies": {"1_year": 10, "2_year": 10, "3_year": 20, "4_year": 30, "5_year": 130} + }, + "short_term_focused": { + "description": 'Most participants choose shorter locks (high penalty scenario)', + "stars": {"1_year": 20000, "2_year": 15000, "3_year": 4000, "4_year": 800, "5_year": 200}, + "galaxies": {"1_year": 100, "2_year": 80, "3_year": 15, "4_year": 3, "5_year": 2} + }, + "low_participation": { + "description": 'Low overall participation scenario', + "stars": {"1_year": 1000, "2_year": 800, "3_year": 600, "4_year": 400, "5_year": 200}, + "galaxies": {"1_year": 8, "2_year": 6, "3_year": 4, "4_year": 2, "5_year": 5} + } +} diff --git a/lockdrop/display.py b/lockdrop/display.py new file mode 100644 index 0000000..8d04169 --- /dev/null +++ b/lockdrop/display.py @@ -0,0 +1,265 @@ +""" +Display and table formatting functions for lockdrop analysis. + +This module contains functions for displaying analysis results in formatted tables +and organizing the presentation of calculation results. +""" + +import pandas as pd +from decimal import Decimal +from tabulate import tabulate +from .constants import ( + PENALTY_RATES, LOCKDROP_ALLOCATION, STAR_ALLOCATION_PERCENT, + GALAXY_ALLOCATION_PERCENT, LOCKDROP_DURATION_BLOCKS, TOTAL_SUPPLY, + NUM_STARS, NUM_GALAXIES, NUM_PLANETS, LOCKDROP_ALLOCATION_PERCENT +) + + +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 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 (excl.)'], + 'Count': [f"{NUM_GALAXIES:,}", f"{NUM_STARS:,}", f"{NUM_PLANETS:,}"], + 'Allocation %': [f"{GALAXY_ALLOCATION_PERCENT:.3%}", f"{STAR_ALLOCATION_PERCENT:.3%}", "0%"] + }) + print_table_with_borders(points_df, "š URBIT POINT DISTRIBUTION") + + # Penalty Schedule Table + penalty_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]], + 'Token % of Max': ['100%', '80%', '60%', '40%', '20%'] + }) + print_table_with_borders(penalty_df, "āļø PENALTY SCHEDULE") + print("\n" + "="*80) + + +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'] + rounding_error_per_star = allocation_data['rounding_error_per_star'] + rounding_error_per_galaxy = allocation_data['rounding_error_per_galaxy'] + 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) + 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) diff --git a/lockdrop/simulation.py b/lockdrop/simulation.py new file mode 100644 index 0000000..dab4fd5 --- /dev/null +++ b/lockdrop/simulation.py @@ -0,0 +1,129 @@ +""" +Simulation-specific functions for processing zenith-stack generated data. + +This module contains functions specifically for processing simulation data +generated by the zenith-stack project, including watcher events and participant data. +""" + +import json +import os +import urbitob +from decimal import Decimal +from collections import defaultdict + +from .constants import NUM_GALAXIES +from .calculations import ( + calculate_dynamic_allocations, calculate_bonus_pools, + calculate_final_allocations, generate_test_output +) +from .display import print_analysis_tables + + +def simulation_load_watcher_events(file_path): + """Load and parse PointLockedEvent events from JSON file (simulation-specific).""" + with open(file_path, 'r') as f: + data = json.load(f) + + # Filter for only PointLockedEvent events during loading + point_locked_events = [ + event_data for event_data in data['data']['eventsInRange'] + if event_data['event']['__typename'] == 'PointLockedEvent' + ] + + return point_locked_events + + +def simulation_analyze_lockdrop_events(events): + """Analyze PointLockedEvent events and return participation statistics (simulation-specific).""" + # Initialize counters + lock_duration_counts = { + 'star': defaultdict(int), + 'galaxy': defaultdict(int) + } + + # Process events (already filtered to PointLockedEvent only) + for event_data in events: + point = event_data['event']['point'] + lock_period = event_data['event']['lock_period'] + + # Determine if it's a galaxy or star + point_num = urbitob.patp_to_num(point) + point_type = "galaxy" if point_num < NUM_GALAXIES else "star" + + # Count by lock period + lock_duration_counts[point_type][lock_period] += 1 + + # Extract counts for each year and point type + 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 + + +def run_simulation_analysis(generated_dir=None): + """Complete simulation analysis pipeline (simulation-specific).""" + # Determine generated directory + if generated_dir is None: + generated_dir = os.getenv('GENERATED_DIR', 'generated') + + # Load and analyze watcher events + watcher_events_path = os.path.join(generated_dir, 'watcher-events.json') + + print("=" * 80) + print("š WATCHER EVENTS DATA SUMMARY") + print("=" * 80) + + try: + point_locked_events = simulation_load_watcher_events(watcher_events_path) + + if point_locked_events: + total_events = len(point_locked_events) + print(f"ā Successfully loaded {total_events} PointLockedEvent records from watcher file") + else: + print("ā ļø No PointLockedEvent events found") + return None + + # Analyze events and convert to Decimal + lock_stats = simulation_analyze_lockdrop_events(point_locked_events) + participation_counts = {k: Decimal(v) for k, v in lock_stats.items()} + + # Run calculations + allocation_data = calculate_dynamic_allocations(participation_counts) + bonus_data = calculate_bonus_pools(allocation_data) + final_data = calculate_final_allocations(allocation_data, bonus_data) + + # Print comprehensive analysis + print_analysis_tables(allocation_data, bonus_data, final_data) + + # Generate and save test output + test_output = generate_test_output(final_data) + allocations_output_file = 'lockdrop_allocations_notebook.json' + + export_data = { + 'metadata': { + 'source': 'lockdrop-calculations-simulated.ipynb', + 'participation_counts': lock_stats + }, + 'allocations': test_output + } + + with open(allocations_output_file, 'w') as f: + json.dump(export_data, f, indent=2) + + print("=" * 80) + print("š¾ JSON OUTPUT GENERATED") + print(f"ā Final allocations saved to: {allocations_output_file}") + print("=" * 80) + + # Return data for visualization + return allocation_data, final_data + + except FileNotFoundError: + print(f"ā Watcher events file not found: {watcher_events_path}") + print(" Make sure GENERATED_DIR environment variable points to the correct directory") + return None + except Exception as e: + print(f"ā Error during simulation analysis: {e}") + return None diff --git a/lockdrop/visualization.py b/lockdrop/visualization.py new file mode 100644 index 0000000..6111f9b --- /dev/null +++ b/lockdrop/visualization.py @@ -0,0 +1,121 @@ +""" +Visualization functions for lockdrop analysis. + +This module contains functions for creating charts and plots to visualize +lockdrop participation and allocation data. +""" + +import matplotlib.pyplot as plt +import seaborn as sns + + +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 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() diff --git a/lockdrop/widgets.py b/lockdrop/widgets.py new file mode 100644 index 0000000..d68628c --- /dev/null +++ b/lockdrop/widgets.py @@ -0,0 +1,304 @@ +""" +Jupyter widget functions for interactive lockdrop experimentation. + +This module contains functions for creating and managing interactive widgets +for the experimental lockdrop notebook. +""" + +import json +import traceback +import ipywidgets as widgets +from datetime import datetime +from IPython.display import clear_output + +from .constants import NUM_STARS, NUM_GALAXIES, SCENARIOS +from .calculations import ( + calculate_dynamic_allocations, calculate_bonus_pools, + calculate_final_allocations, generate_test_output +) +from .display import print_analysis_tables, print_table_with_borders +from .visualization import create_visualization + + +def create_experiment_widgets(): + """Create input widgets for the experimental notebook.""" + # 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 with bounds + stars_1_years = widgets.BoundedIntText(value=default_participants['stars_1_years'], min=0, max=NUM_STARS, description='1 Year:', style={'description_width': '80px'}) + stars_2_years = widgets.BoundedIntText(value=default_participants['stars_2_years'], min=0, max=NUM_STARS, description='2 Years:', style={'description_width': '80px'}) + stars_3_years = widgets.BoundedIntText(value=default_participants['stars_3_years'], min=0, max=NUM_STARS, description='3 Years:', style={'description_width': '80px'}) + stars_4_years = widgets.BoundedIntText(value=default_participants['stars_4_years'], min=0, max=NUM_STARS, description='4 Years:', style={'description_width': '80px'}) + stars_5_years = widgets.BoundedIntText(value=default_participants['stars_5_years'], min=0, max=NUM_STARS, 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.BoundedIntText(value=default_participants['galaxies_1_years'], min=0, max=NUM_GALAXIES, description='1 Year:', style={'description_width': '80px'}) + galaxies_2_years = widgets.BoundedIntText(value=default_participants['galaxies_2_years'], min=0, max=NUM_GALAXIES, description='2 Years:', style={'description_width': '80px'}) + galaxies_3_years = widgets.BoundedIntText(value=default_participants['galaxies_3_years'], min=0, max=NUM_GALAXIES, description='3 Years:', style={'description_width': '80px'}) + galaxies_4_years = widgets.BoundedIntText(value=default_participants['galaxies_4_years'], min=0, max=NUM_GALAXIES, description='4 Years:', style={'description_width': '80px'}) + galaxies_5_years = widgets.BoundedIntText(value=default_participants['galaxies_5_years'], min=0, max=NUM_GALAXIES, 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', '')] + [(scenario['description'], k) for k, scenario 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']] + + # Update widget values + stars_1_years.value = scenario['stars']['1_year'] + stars_2_years.value = scenario['stars']['2_year'] + stars_3_years.value = scenario['stars']['3_year'] + stars_4_years.value = scenario['stars']['4_year'] + stars_5_years.value = scenario['stars']['5_year'] + galaxies_1_years.value = scenario['galaxies']['1_year'] + galaxies_2_years.value = scenario['galaxies']['2_year'] + galaxies_3_years.value = scenario['galaxies']['3_year'] + galaxies_4_years.value = scenario['galaxies']['4_year'] + galaxies_5_years.value = scenario['galaxies']['5_year'] + + 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.""" + # 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.""" + from decimal import Decimal + import pandas as pd + + def calculate_and_display(button=None): + """Calculate allocations and display results based on input parameters.""" + + 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)}") + traceback.print_exc() + + return calculate_and_display + + +def create_experimental_interface(): + """Create complete experimental interface with widgets and handlers.""" + from IPython.display import display + + # Create all widgets + widget_dict = create_experiment_widgets() + + # Create calculation function + calculate_function = create_calculate_function(widget_dict) + + # Wire up the calculate button + widget_dict['calculate_button'].on_click(calculate_function) + + # Display the interface - preset dropdown first + preset_label = widgets.HTML('