Move lockdrop calculations to a module and add a experimentation notebook #2

Merged
nabarun merged 12 commits from pm-experimental-noteboook into main 2025-08-13 11:56:53 +00:00
14 changed files with 1425 additions and 1098 deletions

120
EXPERIMENT.md Normal file
View File

@ -0,0 +1,120 @@
# Lockdrop Allocation Experimentation
This guide explains how to use the interactive notebook to experiment with different lockdrop participation scenarios and understand how various distributions affect token allocations.
## Prerequisites
- [Python3](https://wiki.python.org/moin/BeginnersGuide/Download) `3.12.x` >= `python3 --version` >= `3.8.10` (the Python3 shipped in Ubuntu 20+ is good to go)
- [pip](https://pip.pypa.io/en/stable/installation/) (Python package manager)
## Quick Start
1. **Clone and Navigate to Directory**
```bash
git clone git@git.vdb.to:LaconicNetwork/lockdrop-simulation.git
cd lockdrop-simulation
```
2. **Run the Experimentation Script**
```bash
./run_experiment.sh
```
This script will:
- Automatically create a Python virtual environment
- Install all required dependencies
- Launch Jupyter notebook with the experimentation interface
- Open your browser to the interactive notebook
3. **Use the Interactive Interface**
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
### Input Controls
- **Star Participation**: Adjust participation numbers for stars across 1-5 year lock durations
- **Galaxy Participation**: Adjust participation numbers for galaxies across 1-5 year lock durations
- **Preset Scenarios**: Use the dropdown to quickly load common scenarios:
- **Balanced**: Even distribution across all lock periods
- **Five-year focused**: Most participants choose maximum lock duration
- **Short-term focused**: Most participants choose shorter lock durations
- **Low participation**: Reduced overall participation scenario
### Action Buttons
- **🔄 Calculate Allocations**: Run calculations and display comprehensive analysis
- **💾 Export Results**: Save experimental results to timestamped JSON file
## Understanding the Analysis Output
The notebook provides detailed analysis including:
### 1. Participation Distribution
- Breakdown of participants by lock period
- Participation rates for stars and galaxies
- Total participant counts
### 2. Allocation Calculations
- Base allocations accounting for actual participation
- Quanta-adjusted allocations per participant
- Calculations on rounding errors resulting from quanta usage
### 3. Penalty System Analysis
- Impact of lock duration on allocations before bonuses
- Penalty rates for each lock period
- Allocation percentages relative to maximum
### 4. Bonus Pool Calculations
- How penalties redistribute to 5-year participants
- Bonus amounts per 5-year participant
- Total bonus pool distribution
### 5. Final Allocations & Verification
- Complete allocation breakdown by lock period
- Z tokens per block calculations
- Allocation verification and rounding error tracking
### 6. Visualizations
- Participation distribution charts
- Allocation comparison graphs
- Lock period analysis plots
## Experimenting with Scenarios
### Quick Start with Presets
1. Select a preset from the dropdown (e.g., "Five-year focused")
2. Click "🔄 Calculate Allocations"
3. Review the analysis output
4. Try different presets to compare scenarios
### Custom Scenarios
1. Manually adjust participation numbers
2. Ensure total participants don't exceed maximum available (65,280 stars, 256 galaxies)
3. Click "🔄 Calculate Allocations"
4. Use "💾 Export Results" to save interesting configurations
## Exported Results
Export files contain:
- **Metadata**: Timestamp, source notebook, participation parameters
- **Allocations**: Complete calculation results in JSON format
- **File naming**: `lockdrop_calculations_result_YYYYMMDD_HHMMSS.json`
These files can be used for:
- Further analysis
- Sharing experimental configurations
- Testing different scenarios
- Integration with other tools
## Troubleshooting
- **Import errors**: Ensure all dependencies are installed via `pip install -r requirements.txt`
- **Widget display issues**: Restart notebook kernel and re-run cells
- **Calculation errors**: Check that participation numbers don't exceed limits
- **Export failures**: Ensure write permissions in the notebook directory

View File

@ -1,5 +1,22 @@
# lockdrop-simulation
## Overview
This repository provides two main ways to work with lockdrop calculations:
### Interactive Experimentation
For independent experimentation with lockdrop allocation scenarios, see [EXPERIMENT.md](./EXPERIMENT.md). This allows you to:
- Experiment with different participation distributions using an interactive Jupyter notebook
- Test various scenarios (balanced, five-year focused, short-term focused, etc.)
- Export timestamped results for analysis
### Full Simulation & Validation
Continue reading below for the complete simulation workflow that validates token distribution against a live zenithd node.
---
## Approach
The lockdrop simulation validates the Zenith Network's token distribution mechanism by creating a realistic test environment without requiring real Ethereum data or live participants.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,90 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "experimental-header",
"metadata": {},
"source": [
"# Z Token Lockdrop Distribution\n",
"\n",
"This notebook allows you to experiment with different participation distributions to see how they affect token allocations, bonus pools, and the penalty system.\n",
"\n",
"**Features:**\n",
"- Adjust star and galaxy participation numbers by lock duration\n",
"- Real-time calculation of allocations and bonus pools\n",
"- Preset scenarios available in dropdown (balanced, five-year focused, short-term focused, low participation)\n",
"- Visualization of results"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "setup-imports",
"metadata": {},
"outputs": [],
"source": [
"# Import shared calculation module\n",
"from lockdrop import (\n",
" configure_matplotlib, print_constants_summary,\n",
" create_experimental_interface\n",
")\n",
"\n",
"# Configure matplotlib\n",
"configure_matplotlib()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "show-constants",
"metadata": {},
"outputs": [],
"source": [
"# Display core constants\n",
"print_constants_summary()"
]
},
{
"cell_type": "markdown",
"id": "input-section-header",
"metadata": {},
"source": [
"## 🎛️ Experimental Parameters\n",
"\n",
"Adjust the participation numbers below to experiment with different scenarios. Use the preset dropdown to quickly load common scenarios, or manually adjust the values for custom experiments."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "input-parameters",
"metadata": {},
"outputs": [],
"source": [
"# Create complete experimental interface\n",
"create_experimental_interface()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

25
lockdrop/__init__.py Normal file
View File

@ -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

185
lockdrop/calculations.py Normal file
View File

@ -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": int(total_allocation * Decimal('1e8'))
}
return output

64
lockdrop/constants.py Normal file
View File

@ -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}
}
}

265
lockdrop/display.py Normal file
View File

@ -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)

129
lockdrop/simulation.py Normal file
View File

@ -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

121
lockdrop/visualization.py Normal file
View File

@ -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()

304
lockdrop/widgets.py Normal file
View File

@ -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('<h3>🎮 Preset Scenarios</h3>')
preset_layout = widgets.VBox([preset_label, widget_dict['scenario_dropdown']])
# Then participation controls
star_label = widgets.HTML('<h3>⭐ Star Participation</h3>')
galaxy_label = widgets.HTML('<h3>🌌 Galaxy Participation</h3>')
input_layout = widgets.HBox([
widgets.VBox([star_label, widget_dict['star_controls']]),
widgets.VBox([galaxy_label, widget_dict['galaxy_controls']])
])
# Action buttons
controls_layout = widgets.HBox([
widget_dict['calculate_button'],
widget_dict['export_button']
])
# Display in order: preset first, then participation controls, then buttons
display(preset_layout)
display(input_layout)
display(controls_layout)
display(widget_dict['output_area'])
return None

View File

@ -6,3 +6,4 @@ numpy
urbitob
tabulate
requests
ipywidgets

49
run_experiment.sh Executable file
View File

@ -0,0 +1,49 @@
#!/bin/bash
# Lockdrop Allocation Experimentation Launcher
# This script automatically sets up the environment and launches the experimental notebook
set -e
echo "🚀 Starting Lockdrop Experimentation Environment..."
# Check if Python 3 is available
if ! command -v python3 &> /dev/null; then
echo "❌ Python 3 is required but not installed. Please install Python 3."
exit 1
fi
# Create virtual environment if it doesn't exist
if [ ! -d "venv" ]; then
echo "📦 Creating Python virtual environment..."
python3 -m venv venv
fi
# Activate virtual environment
echo "🔧 Activating virtual environment..."
source venv/bin/activate
# Install/upgrade dependencies
echo "📚 Installing dependencies..."
pip install -q --upgrade pip
pip install -q -r requirements.txt
# Check if Jupyter is working
if ! command -v jupyter &> /dev/null; then
echo "❌ Jupyter installation failed. Please check your Python environment."
exit 1
fi
echo "✅ Environment ready!"
echo ""
echo "🎯 Opening lockdrop experimentation notebook..."
echo " The notebook will open in your default browser at http://localhost:8888"
echo ""
echo "📝 To stop the notebook server later, press Ctrl+C in this terminal"
echo ""
# Launch Jupyter notebook
jupyter notebook lockdrop-calculations.ipynb
echo ""
echo "👋 Experimentation session ended."

View File

@ -31,7 +31,8 @@ class BaseAllocationTest(unittest.TestCase):
with open(f'{cls.generated_dir}/generated-participants.json', 'r') as f:
cls.participants = json.load(f)
with open('lockdrop_allocations_notebook.json', 'r') as f:
cls.notebook_allocations = json.load(f)
notebook_data = json.load(f)
cls.notebook_allocations = notebook_data['allocations']
cls.points_by_duration = cls._get_first_points()