From 48bb2f7b83c5eb1854970517ee25d57eca8092e5 Mon Sep 17 00:00:00 2001 From: Prathamesh Musale Date: Tue, 12 Aug 2025 15:09:55 +0530 Subject: [PATCH] Refactor lockdrop calculations to a module --- lockdrop-calculations-simulated.ipynb | 4 +- lockdrop-calculations.ipynb | 608 ++++++++++++++++++++++++++ lockdrop_calculations.py | 500 +++++++++++++++++++++ requirements.txt | 1 + 4 files changed, 1111 insertions(+), 2 deletions(-) create mode 100644 lockdrop-calculations.ipynb create mode 100644 lockdrop_calculations.py diff --git a/lockdrop-calculations-simulated.ipynb b/lockdrop-calculations-simulated.ipynb index 61cc331..ff5cae9 100644 --- a/lockdrop-calculations-simulated.ipynb +++ b/lockdrop-calculations-simulated.ipynb @@ -424,7 +424,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "26573d6b", "metadata": {}, "outputs": [ @@ -489,7 +489,7 @@ "# Load events from watcher file\n", "import os\n", "\n", - "watcher_events_path = os.path.join(os.getenv('GENERATED_DIR', './generated'), 'watcher-events.json')\n", + "watcher_events_path = os.path.join(os.getenv('GENERATED_DIR', 'generated'), 'watcher-events.json')\n", "events = load_watcher_events(watcher_events_path)\n", "lock_stats = analyze_lockdrop_events(events)\n", "\n", diff --git a/lockdrop-calculations.ipynb b/lockdrop-calculations.ipynb new file mode 100644 index 0000000..9637137 --- /dev/null +++ b/lockdrop-calculations.ipynb @@ -0,0 +1,608 @@ +{ + "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", + "- Visualization of results" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "setup-imports", + "metadata": {}, + "outputs": [], + "source": [ + "# Import shared calculation module\n", + "from lockdrop_calculations import (\n", + " configure_matplotlib, print_constants_summary, calculate_dynamic_allocations,\n", + " calculate_bonus_pools, calculate_final_allocations, generate_test_output,\n", + " print_analysis_tables, create_visualization, PENALTY_RATES, NUM_STARS, NUM_GALAXIES\n", + ")\n", + "import json\n", + "from decimal import Decimal\n", + "import pandas as pd\n", + "import ipywidgets as widgets\n", + "from IPython.display import display, clear_output\n", + "\n", + "# Configure matplotlib\n", + "configure_matplotlib()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "show-constants", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "================================================================================\n", + "๐Ÿ“Š $Z LOCKDROP DISTRIBUTION - CORE CONSTANTS\n", + "================================================================================\n", + "\n", + "๐Ÿ”’ LOCKDROP ALLOCATION\n", + " Parameter Value\n", + "Total Supply (1 $Z per Urbit ID) 4,294,967,296\n", + " Lockdrop Allocation % 30.0%\n", + " Lockdrop Allocation ($Z) 1,288,490,188.8\n", + "\n", + "โญ URBIT POINTS DISTRIBUTION\n", + "Point Type Count Allocation %\n", + " Galaxies 256 0.39%\n", + " Stars 65,280 99.61%\n", + " Planets 4,294,901,760 0%\n", + "\n", + "โฑ๏ธ LOCKDROP PARAMETERS\n", + " Parameter Value\n", + " Block Duration 2 seconds\n", + "Max Point Lock Duration (5 yrs) 157,788,000 seconds\n", + " Total Blocks 78,894,000\n", + " Star Allocation % 0.996093\n", + " Galaxy Allocation % 0.003906\n", + "\n", + "================================================================================\n" + ] + } + ], + "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. The default values represent a balanced distribution scenario." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "input-parameters", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "๐ŸŽฏ PARTICIPATION INPUT CONTROLS\n", + "==================================================\n", + "\n", + "โญ STAR PARTICIPATION\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "79244d07df4e4ee6923dda69c1febc2e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(IntText(value=8000, description='1 Year:', style=DescriptionStyle(description_width='80px')), Iโ€ฆ" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "๐ŸŒŒ GALAXY PARTICIPATION\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4071b25411c44a67b26f5924a2507c0c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(IntText(value=40, description='1 Year:', style=DescriptionStyle(description_width='80px')), Intโ€ฆ" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1c785fe3828d4d5eb62a9f35755f29f8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Button(button_style='success', description='๐Ÿ”„ Calculate Allocations', layout=Layout(margin='20px 0', width='20โ€ฆ" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "๐ŸŒŒ CALCULATIONS\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bc3937e163a34ff58f215f94a36aee3e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Define reasonable default values\n", + "DEFAULT_PARTICIPANTS = {\n", + " 'stars_1_years': 8000,\n", + " 'stars_2_years': 8000,\n", + " 'stars_3_years': 8000,\n", + " 'stars_4_years': 8000,\n", + " 'stars_5_years': 8000,\n", + " 'galaxies_1_years': 40,\n", + " 'galaxies_2_years': 40,\n", + " 'galaxies_3_years': 40,\n", + " 'galaxies_4_years': 40,\n", + " 'galaxies_5_years': 40\n", + "}\n", + "\n", + "# Create input widgets\n", + "print(\"๐ŸŽฏ PARTICIPATION INPUT CONTROLS\")\n", + "print(\"=\"*50)\n", + "\n", + "# Star participation widgets\n", + "print(\"\\nโญ STAR PARTICIPATION\")\n", + "stars_1_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['stars_1_years'], description='1 Year:', style={'description_width': '80px'})\n", + "stars_2_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['stars_2_years'], description='2 Years:', style={'description_width': '80px'})\n", + "stars_3_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['stars_3_years'], description='3 Years:', style={'description_width': '80px'})\n", + "stars_4_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['stars_4_years'], description='4 Years:', style={'description_width': '80px'})\n", + "stars_5_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['stars_5_years'], description='5 Years:', style={'description_width': '80px'})\n", + "\n", + "star_controls = widgets.VBox([stars_1_years, stars_2_years, stars_3_years, stars_4_years, stars_5_years])\n", + "display(star_controls)\n", + "\n", + "print(\"\\n๐ŸŒŒ GALAXY PARTICIPATION\")\n", + "galaxies_1_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['galaxies_1_years'], description='1 Year:', style={'description_width': '80px'})\n", + "galaxies_2_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['galaxies_2_years'], description='2 Years:', style={'description_width': '80px'})\n", + "galaxies_3_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['galaxies_3_years'], description='3 Years:', style={'description_width': '80px'})\n", + "galaxies_4_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['galaxies_4_years'], description='4 Years:', style={'description_width': '80px'})\n", + "galaxies_5_years = widgets.IntText(value=DEFAULT_PARTICIPANTS['galaxies_5_years'], description='5 Years:', style={'description_width': '80px'})\n", + "\n", + "galaxy_controls = widgets.VBox([galaxies_1_years, galaxies_2_years, galaxies_3_years, galaxies_4_years, galaxies_5_years])\n", + "display(galaxy_controls)\n", + "\n", + "# Calculate button\n", + "calculate_button = widgets.Button(description='๐Ÿ”„ Calculate Allocations', button_style='success', layout={'width': '200px', 'margin': '20px 0'})\n", + "display(calculate_button)\n", + "\n", + "# Output area\n", + "print(\"\\n๐ŸŒŒ CALCULATIONS\")\n", + "output_area = widgets.Output()\n", + "display(output_area)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "calculation-function", + "metadata": {}, + "outputs": [], + "source": [ + "def calculate_and_display(button=None):\n", + " \"\"\"Calculate allocations and display results based on input parameters.\"\"\"\n", + " with output_area:\n", + " clear_output(wait=True)\n", + "\n", + " # Get current values from widgets\n", + " participation_counts = {\n", + " 'stars_1_years': stars_1_years.value,\n", + " 'stars_2_years': stars_2_years.value,\n", + " 'stars_3_years': stars_3_years.value,\n", + " 'stars_4_years': stars_4_years.value,\n", + " 'stars_5_years': stars_5_years.value,\n", + " 'galaxies_1_years': galaxies_1_years.value,\n", + " 'galaxies_2_years': galaxies_2_years.value,\n", + " 'galaxies_3_years': galaxies_3_years.value,\n", + " 'galaxies_4_years': galaxies_4_years.value,\n", + " 'galaxies_5_years': galaxies_5_years.value\n", + " }\n", + "\n", + " # Validate inputs\n", + " total_stars = sum(participation_counts[key] for key in participation_counts if 'stars' in key)\n", + " total_galaxies = sum(participation_counts[key] for key in participation_counts if 'galaxies' in key)\n", + "\n", + " if total_stars > NUM_STARS:\n", + " print(f\"โš ๏ธ ERROR: Total stars ({total_stars:,}) exceeds maximum available ({NUM_STARS:,})\")\n", + " return\n", + "\n", + " if total_galaxies > NUM_GALAXIES:\n", + " print(f\"โš ๏ธ ERROR: Total galaxies ({total_galaxies:,}) exceeds maximum available ({NUM_GALAXIES:,})\")\n", + " return\n", + "\n", + " if total_stars == 0 and total_galaxies == 0:\n", + " print(\"โš ๏ธ ERROR: Must have at least some participants\")\n", + " return\n", + "\n", + " try:\n", + " # Perform calculations\n", + " print(\"๐Ÿ”„ Calculating allocations...\\n\")\n", + "\n", + " allocation_data = calculate_dynamic_allocations(participation_counts)\n", + " bonus_data = calculate_bonus_pools(allocation_data)\n", + " final_data = calculate_final_allocations(allocation_data, bonus_data)\n", + "\n", + " # Display results\n", + " print_analysis_tables(allocation_data, bonus_data, final_data)\n", + "\n", + " # Show penalty rates for reference\n", + " print(\"\\n๐Ÿ“‹ PENALTY SYSTEM REFERENCE\")\n", + " penalty_df = pd.DataFrame({\n", + " 'Lock Period': ['5 Years', '4 Years', '3 Years', '2 Years', '1 Year'],\n", + " 'Penalty Rate': [f\"{PENALTY_RATES[year]:.1%}\" for year in [5, 4, 3, 2, 1]],\n", + " 'Description': ['No penalty + Bonus', '20% penalty', '40% penalty', '60% penalty', '80% penalty']\n", + " })\n", + " print(penalty_df.to_string(index=False))\n", + "\n", + " # Create visualization\n", + " print(\"\\n๐Ÿ“Š Generating visualization...\")\n", + " create_visualization(allocation_data, final_data)\n", + "\n", + " # Calculate participation insights\n", + " star_participation_rate = Decimal(total_stars) / NUM_STARS\n", + " galaxy_participation_rate = Decimal(total_galaxies) / NUM_GALAXIES\n", + "\n", + " print(\"\\n\" + \"=\"*80)\n", + " print(\"๐ŸŽฏ EXPERIMENT INSIGHTS\")\n", + " print(\"=\"*80)\n", + "\n", + " insights_df = pd.DataFrame({\n", + " 'Metric': [\n", + " 'Total Participants',\n", + " 'Star Participation Rate',\n", + " 'Galaxy Participation Rate',\n", + " '5-Year Star Bonus',\n", + " '5-Year Galaxy Bonus',\n", + " 'Highest Individual Allocation',\n", + " 'Lowest Individual Allocation'\n", + " ],\n", + " 'Value': [\n", + " f\"{total_stars + total_galaxies:,}\",\n", + " f\"{star_participation_rate:.1%}\",\n", + " f\"{galaxy_participation_rate:.1%}\",\n", + " f\"{float(bonus_data['bonus_per_star_5_years']):,.2f} $Z\" if participation_counts['stars_5_years'] > 0 else \"N/A (no 5Y stars)\",\n", + " f\"{float(bonus_data['bonus_per_galaxy_5_years']):,.2f} $Z\" if participation_counts['galaxies_5_years'] > 0 else \"N/A (no 5Y galaxies)\",\n", + " f\"{max(float(final_data['final_star_allocations'][5]), float(final_data['final_galaxy_allocations'][5])):,.2f} $Z\",\n", + " f\"{min(float(final_data['final_star_allocations'][1]), float(final_data['final_galaxy_allocations'][1])):,.2f} $Z\"\n", + " ]\n", + " })\n", + " print(insights_df.to_string(index=False))\n", + " print(\"\\n\" + \"=\"*80)\n", + "\n", + " except Exception as e:\n", + " print(f\"โŒ Error during calculation: {str(e)}\")\n", + " import traceback\n", + " traceback.print_exc()\n", + "\n", + "# Connect button to calculation function\n", + "calculate_button.on_click(calculate_and_display)" + ] + }, + { + "cell_type": "markdown", + "id": "preset-scenarios-header", + "metadata": {}, + "source": [ + "## ๐ŸŽฎ Preset Scenarios\n", + "\n", + "Try these interesting scenarios by running the code below:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "preset-scenarios", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "๐Ÿ“‹ AVAILABLE PRESET SCENARIOS:\n", + "==================================================\n", + "\n", + "โ€ข balanced: Balanced distribution across all lock periods\n", + "\n", + "โ€ข five_year_focused: Most participants choose 5-year lock (maximum bonus scenario)\n", + "\n", + "โ€ข short_term_focused: Most participants choose shorter locks (high penalty scenario)\n", + "\n", + "โ€ข polarized: Split between 1-year and 5-year locks (maximum bonus per 5Y participant)\n", + "\n", + "โ€ข low_participation: Low overall participation scenario\n", + "\n", + "๐ŸŽฏ To load a scenario, call: load_scenario('scenario_name')\n", + "\n", + "Example: load_scenario('five_year_focused')\n" + ] + } + ], + "source": [ + "# Define preset scenarios\n", + "SCENARIOS = {\n", + " 'balanced': {\n", + " 'description': 'Balanced distribution across all lock periods',\n", + " 'params': {\n", + " 'stars_1_years': 8000, 'stars_2_years': 8000, 'stars_3_years': 8000, 'stars_4_years': 8000, 'stars_5_years': 8000,\n", + " 'galaxies_1_years': 40, 'galaxies_2_years': 40, 'galaxies_3_years': 40, 'galaxies_4_years': 40, 'galaxies_5_years': 40\n", + " }\n", + " },\n", + " 'five_year_focused': {\n", + " 'description': 'Most participants choose 5-year lock (maximum bonus scenario)',\n", + " 'params': {\n", + " 'stars_1_years': 2000, 'stars_2_years': 2000, 'stars_3_years': 3000, 'stars_4_years': 5000, 'stars_5_years': 28000,\n", + " 'galaxies_1_years': 10, 'galaxies_2_years': 10, 'galaxies_3_years': 20, 'galaxies_4_years': 30, 'galaxies_5_years': 130\n", + " }\n", + " },\n", + " 'short_term_focused': {\n", + " 'description': 'Most participants choose shorter locks (high penalty scenario)',\n", + " 'params': {\n", + " 'stars_1_years': 20000, 'stars_2_years': 15000, 'stars_3_years': 4000, 'stars_4_years': 800, 'stars_5_years': 200,\n", + " 'galaxies_1_years': 100, 'galaxies_2_years': 80, 'galaxies_3_years': 15, 'galaxies_4_years': 3, 'galaxies_5_years': 2\n", + " }\n", + " },\n", + " 'polarized': {\n", + " 'description': 'Split between 1-year and 5-year locks (maximum bonus per 5Y participant)',\n", + " 'params': {\n", + " 'stars_1_years': 25000, 'stars_2_years': 0, 'stars_3_years': 0, 'stars_4_years': 0, 'stars_5_years': 15000,\n", + " 'galaxies_1_years': 150, 'galaxies_2_years': 0, 'galaxies_3_years': 0, 'galaxies_4_years': 0, 'galaxies_5_years': 50\n", + " }\n", + " },\n", + " 'low_participation': {\n", + " 'description': 'Low overall participation scenario',\n", + " 'params': {\n", + " 'stars_1_years': 1000, 'stars_2_years': 800, 'stars_3_years': 600, 'stars_4_years': 400, 'stars_5_years': 200,\n", + " 'galaxies_1_years': 8, 'galaxies_2_years': 6, 'galaxies_3_years': 4, 'galaxies_4_years': 2, 'galaxies_5_years': 5\n", + " }\n", + " }\n", + "}\n", + "\n", + "def load_scenario(scenario_name):\n", + " \"\"\"Load a preset scenario into the input widgets.\"\"\"\n", + " if scenario_name not in SCENARIOS:\n", + " print(f\"โŒ Unknown scenario: {scenario_name}\")\n", + " return\n", + "\n", + " scenario = SCENARIOS[scenario_name]\n", + " params = scenario['params']\n", + "\n", + " # Update widget values\n", + " stars_1_years.value = params['stars_1_years']\n", + " stars_2_years.value = params['stars_2_years']\n", + " stars_3_years.value = params['stars_3_years']\n", + " stars_4_years.value = params['stars_4_years']\n", + " stars_5_years.value = params['stars_5_years']\n", + " galaxies_1_years.value = params['galaxies_1_years']\n", + " galaxies_2_years.value = params['galaxies_2_years']\n", + " galaxies_3_years.value = params['galaxies_3_years']\n", + " galaxies_4_years.value = params['galaxies_4_years']\n", + " galaxies_5_years.value = params['galaxies_5_years']\n", + "\n", + " print(f\"โœ… Loaded scenario: {scenario_name}\")\n", + " print(f\"๐Ÿ“ Description: {scenario['description']}\")\n", + " print(\"\\n๐Ÿ”„ Click 'Calculate Allocations' button above to see results!\")\n", + "\n", + "# Display available scenarios\n", + "print(\"๐Ÿ“‹ AVAILABLE PRESET SCENARIOS:\")\n", + "print(\"=\"*50)\n", + "for name, scenario in SCENARIOS.items():\n", + " print(f\"\\nโ€ข {name}: {scenario['description']}\")\n", + "\n", + "print(\"\\n๐ŸŽฏ To load a scenario, call: load_scenario('scenario_name')\")\n", + "print(\"\\nExample: load_scenario('five_year_focused')\")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "load-scenario-example", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "โœ… Loaded scenario: five_year_focused\n", + "๐Ÿ“ Description: Most participants choose 5-year lock (maximum bonus scenario)\n", + "\n", + "๐Ÿ”„ Click 'Calculate Allocations' button above to see results!\n" + ] + } + ], + "source": [ + "# Load the balanced scenario as default\n", + "load_scenario('five_year_focused')" + ] + }, + { + "cell_type": "markdown", + "id": "export-section", + "metadata": {}, + "source": [ + "## ๐Ÿ’พ Export Results\n", + "\n", + "Export your experimental results for use with the test suite:" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "export-results", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "๐Ÿ’พ Export your current scenario results:\n", + " export_current_scenario('my_experiment.json')\n", + "\n", + "๐Ÿ“‹ Or use default filename:\n", + " export_current_scenario()\n" + ] + } + ], + "source": [ + "def export_current_scenario(filename='lockdrop_allocations_experiment.json'):\n", + " \"\"\"Export current scenario results to JSON file.\"\"\"\n", + " # Get current values from widgets\n", + " participation_counts = {\n", + " 'stars_1_years': stars_1_years.value,\n", + " 'stars_2_years': stars_2_years.value,\n", + " 'stars_3_years': stars_3_years.value,\n", + " 'stars_4_years': stars_4_years.value,\n", + " 'stars_5_years': stars_5_years.value,\n", + " 'galaxies_1_years': galaxies_1_years.value,\n", + " 'galaxies_2_years': galaxies_2_years.value,\n", + " 'galaxies_3_years': galaxies_3_years.value,\n", + " 'galaxies_4_years': galaxies_4_years.value,\n", + " 'galaxies_5_years': galaxies_5_years.value\n", + " }\n", + "\n", + " try:\n", + " # Calculate allocations\n", + " allocation_data = calculate_dynamic_allocations(participation_counts)\n", + " bonus_data = calculate_bonus_pools(allocation_data)\n", + " final_data = calculate_final_allocations(allocation_data, bonus_data)\n", + "\n", + " # Generate test output\n", + " test_output = generate_test_output(final_data)\n", + "\n", + " # Add metadata\n", + " export_data = {\n", + " 'metadata': {\n", + " 'source': 'lockdrop-experiment.ipynb',\n", + " 'scenario': 'custom',\n", + " 'participation_counts': participation_counts\n", + " },\n", + " 'allocations': test_output\n", + " }\n", + "\n", + " # Save to file\n", + " with open(filename, 'w') as f:\n", + " json.dump(export_data, f, indent=2)\n", + "\n", + " print(f\"โœ… Results exported to: {filename}\")\n", + " print(f\"๐Ÿ“Š Total participants: {sum(participation_counts.values()):,}\")\n", + "\n", + " except Exception as e:\n", + " print(f\"โŒ Export failed: {str(e)}\")\n", + "\n", + "# Usage example\n", + "print(\"๐Ÿ’พ Export your current scenario results:\")\n", + "print(\" export_current_scenario('my_experiment.json')\")\n", + "print(\"\\n๐Ÿ“‹ Or use default filename:\")\n", + "print(\" export_current_scenario()\")" + ] + }, + { + "cell_type": "markdown", + "id": "tips-section", + "metadata": {}, + "source": [ + "## ๐Ÿ’ก Experimentation Tips\n", + "\n", + "### Interesting Things to Try:\n", + "\n", + "1. **Bonus Pool Impact**: Set most participants to 1-2 years and a few to 5 years to see massive bonuses\n", + "2. **Participation Rate Effects**: Try very low participation vs. high participation scenarios\n", + "3. **Star vs Galaxy Balance**: Experiment with different star/galaxy ratios\n", + "4. **Edge Cases**: What happens with only 5-year participants? Only 1-year?\n", + "5. **Realistic Scenarios**: Model expected real-world behavior patterns\n", + "\n", + "### Key Metrics to Watch:\n", + "\n", + "- **5-Year Bonus Multiplier**: How much extra do 5-year participants get?\n", + "- **Participation Rates**: What percentage of each point type participates?\n", + "- **Penalty Pool Size**: How much bonus is generated from penalties?\n", + "- **Allocation Spread**: Difference between highest and lowest allocations\n", + "\n", + "### Understanding the Math:\n", + "\n", + "- Penalties from shorter lock periods create bonus pools\n", + "- All bonus pools go to 5-year participants\n", + "- More penalty = more bonus for 5-year participants\n", + "- Lower participation = higher individual allocations" + ] + } + ], + "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 +} diff --git a/lockdrop_calculations.py b/lockdrop_calculations.py new file mode 100644 index 0000000..e1b6021 --- /dev/null +++ b/lockdrop_calculations.py @@ -0,0 +1,500 @@ +""" +Shared lockdrop calculation functions and utilities. + +This module contains the reference calculation logic for lockdrop token distribution, +penalty systems, bonus pools, and final allocations. It can be used by both +the simulation notebook and the experimental notebook. +""" + +from decimal import Decimal, ROUND_DOWN, getcontext +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import json +import os +from collections import defaultdict + +# Configure decimal precision +getcontext().prec = 28 +getcontext().rounding = ROUND_DOWN + +# Constants +TOKEN_PRECISION = Decimal('0.00000001') # 8 decimal precision +TOTAL_SUPPLY = 4294967296 # 1 $Z per Urbit ID +LOCKDROP_ALLOCATION_PERCENT = Decimal('0.3') +LOCKDROP_ALLOCATION = TOTAL_SUPPLY * LOCKDROP_ALLOCATION_PERCENT + +# Points +NUM_GALAXIES = pow(2, 8) +NUM_STARS = pow(2, 16) - pow(2, 8) +NUM_PLANETS = pow(2, 32) - pow(2, 16) + +# Allocation distribution +STAR_ALLOCATION_PERCENT = Decimal(NUM_STARS / pow(2, 16)) +GALAXY_ALLOCATION_PERCENT = 1 - STAR_ALLOCATION_PERCENT + +# Lockdrop duration (5 years including 1 leap year) +LOCKDROP_DURATION_SECONDS = 5 * 365.25 * 24 * 60 * 60 +BLOCK_DURATION_SECONDS = 2 +LOCKDROP_DURATION_BLOCKS = int(LOCKDROP_DURATION_SECONDS / BLOCK_DURATION_SECONDS) + +# Penalty rates +PENALTY_RATES = { + 5: Decimal('0'), + 4: Decimal('0.2'), + 3: Decimal('0.4'), + 2: Decimal('0.6'), + 1: Decimal('0.8') +} + + +def configure_matplotlib(): + """Configure matplotlib settings for consistent plots.""" + plt.style.use('seaborn-v0_8') + sns.set_palette("husl") + + plt.rcParams['figure.figsize'] = (12, 8) + plt.rcParams['font.size'] = 11 + plt.rcParams['axes.titlesize'] = 14 + plt.rcParams['axes.labelsize'] = 12 + plt.rcParams['xtick.labelsize'] = 10 + plt.rcParams['ytick.labelsize'] = 10 + plt.rcParams['legend.fontsize'] = 10 + plt.rcParams['axes.unicode_minus'] = False + + try: + plt.rcParams['font.family'] = 'DejaVu Sans' + except: + plt.rcParams['font.family'] = 'sans-serif' + plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'Liberation Sans', 'Arial', 'Helvetica'] + + +def print_constants_summary(): + """Display core constants as formatted tables.""" + print("=" * 80) + print("๐Ÿ“Š $Z LOCKDROP DISTRIBUTION - CORE CONSTANTS") + print("=" * 80) + + # Lockdrop Allocation Table + lockdrop_df = pd.DataFrame({ + 'Parameter': ['Total Supply (1 $Z per Urbit ID)', 'Lockdrop Allocation %', 'Lockdrop Allocation ($Z)'], + 'Value': [f"{TOTAL_SUPPLY:,}", f"{LOCKDROP_ALLOCATION_PERCENT:.1%}", f"{LOCKDROP_ALLOCATION:,.1f}"] + }) + print("\n๐Ÿ”’ LOCKDROP ALLOCATION") + print(lockdrop_df.to_string(index=False)) + + # Points Distribution Table + points_df = pd.DataFrame({ + 'Point Type': ['Galaxies', 'Stars', 'Planets'], + 'Count': [f"{NUM_GALAXIES:,}", f"{NUM_STARS:,}", f"{NUM_PLANETS:,}"], + 'Allocation %': ['0.39%', '99.61%', '0%'] + }) + print("\nโญ URBIT POINTS DISTRIBUTION") + print(points_df.to_string(index=False)) + + # Lockdrop Parameters Table + params_df = pd.DataFrame({ + 'Parameter': ['Block Duration', 'Max Point Lock Duration (5 yrs)', 'Total Blocks', 'Star Allocation %', 'Galaxy Allocation %'], + 'Value': [f"{BLOCK_DURATION_SECONDS} seconds", f"{LOCKDROP_DURATION_SECONDS:,.0f} seconds", + f"{LOCKDROP_DURATION_BLOCKS:,}", f"{STAR_ALLOCATION_PERCENT:.6f}", f"{GALAXY_ALLOCATION_PERCENT:.6f}"] + }) + print("\nโฑ๏ธ LOCKDROP PARAMETERS") + print(params_df.to_string(index=False)) + print("\n" + "="*80) + + +def calculate_base_allocations(): + """Calculate base allocation amounts and quanta.""" + lockdrop_allocation_stars = LOCKDROP_ALLOCATION * STAR_ALLOCATION_PERCENT + lockdrop_allocation_galaxies = LOCKDROP_ALLOCATION * GALAXY_ALLOCATION_PERCENT + + assert (lockdrop_allocation_stars + lockdrop_allocation_galaxies) == LOCKDROP_ALLOCATION, "point allocation doesn't add up" + + # Max allocation per point (theoretical) + max_allocation_per_star = lockdrop_allocation_stars / NUM_STARS + max_allocation_per_galaxy = lockdrop_allocation_galaxies / NUM_GALAXIES + + # Quanta calculation + z_available_per_star_per_block = max_allocation_per_star / LOCKDROP_DURATION_BLOCKS + z_available_per_galaxy_per_block = max_allocation_per_galaxy / LOCKDROP_DURATION_BLOCKS + + # Round down to 6 decimals + adjusted_z_per_star_per_block = z_available_per_star_per_block.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) + adjusted_z_per_galaxy_per_block = z_available_per_galaxy_per_block.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) + + # Adjusted max allocation + adjusted_max_allocation_per_star = adjusted_z_per_star_per_block * LOCKDROP_DURATION_BLOCKS + adjusted_max_allocation_per_galaxy = adjusted_z_per_galaxy_per_block * LOCKDROP_DURATION_BLOCKS + + return { + 'lockdrop_allocation_stars': lockdrop_allocation_stars, + 'lockdrop_allocation_galaxies': lockdrop_allocation_galaxies, + 'max_allocation_per_star': max_allocation_per_star, + 'max_allocation_per_galaxy': max_allocation_per_galaxy, + 'adjusted_max_allocation_per_star': adjusted_max_allocation_per_star, + 'adjusted_max_allocation_per_galaxy': adjusted_max_allocation_per_galaxy, + 'adjusted_z_per_star_per_block': adjusted_z_per_star_per_block, + 'adjusted_z_per_galaxy_per_block': adjusted_z_per_galaxy_per_block + } + + +def calculate_dynamic_allocations(participation_counts): + """ + Calculate allocations based on actual participation counts. + + Args: + participation_counts: Dict with keys like 'stars_1_years', 'stars_2_years', etc. + """ + # Extract participation data + stars_counts = {year: Decimal(participation_counts[f'stars_{year}_years']) for year in range(1, 6)} + galaxies_counts = {year: Decimal(participation_counts[f'galaxies_{year}_years']) for year in range(1, 6)} + + total_stars_locked = sum(stars_counts.values()) + total_galaxies_locked = sum(galaxies_counts.values()) + + lockdrop_allocation_stars = LOCKDROP_ALLOCATION * STAR_ALLOCATION_PERCENT + lockdrop_allocation_galaxies = LOCKDROP_ALLOCATION * GALAXY_ALLOCATION_PERCENT + + # Dynamic allocations based on actual participation + max_allocation_per_star = lockdrop_allocation_stars / total_stars_locked if total_stars_locked > 0 else Decimal('0') + max_allocation_per_galaxy = lockdrop_allocation_galaxies / total_galaxies_locked if total_galaxies_locked > 0 else Decimal('0') + + # Quanta calculation + z_available_per_star_per_block = max_allocation_per_star / LOCKDROP_DURATION_BLOCKS + z_available_per_galaxy_per_block = max_allocation_per_galaxy / LOCKDROP_DURATION_BLOCKS + + # Round down to 6 decimals + adjusted_z_per_star_per_block = z_available_per_star_per_block.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) + adjusted_z_per_galaxy_per_block = z_available_per_galaxy_per_block.quantize(Decimal('0.000001'), rounding=ROUND_DOWN) + + # Adjusted max allocation + adjusted_max_allocation_per_star = adjusted_z_per_star_per_block * LOCKDROP_DURATION_BLOCKS + adjusted_max_allocation_per_galaxy = adjusted_z_per_galaxy_per_block * LOCKDROP_DURATION_BLOCKS + + # Rounding errors + rounding_error_per_star = max_allocation_per_star - adjusted_max_allocation_per_star + rounding_error_per_galaxy = max_allocation_per_galaxy - adjusted_max_allocation_per_galaxy + + total_rounding_error_stars = lockdrop_allocation_stars - (adjusted_max_allocation_per_star * total_stars_locked) + total_rounding_error_galaxies = lockdrop_allocation_galaxies - (adjusted_max_allocation_per_galaxy * total_galaxies_locked) + + return { + 'stars_counts': stars_counts, + 'galaxies_counts': galaxies_counts, + 'total_stars_locked': total_stars_locked, + 'total_galaxies_locked': total_galaxies_locked, + 'adjusted_max_allocation_per_star': adjusted_max_allocation_per_star, + 'adjusted_max_allocation_per_galaxy': adjusted_max_allocation_per_galaxy, + 'total_rounding_error_stars': total_rounding_error_stars, + 'total_rounding_error_galaxies': total_rounding_error_galaxies, + 'adjusted_z_per_star_per_block': adjusted_z_per_star_per_block, + 'adjusted_z_per_galaxy_per_block': adjusted_z_per_galaxy_per_block + } + + +def calculate_bonus_pools(allocation_data): + """Calculate bonus pools from penalties and rounding errors.""" + stars_counts = allocation_data['stars_counts'] + galaxies_counts = allocation_data['galaxies_counts'] + adjusted_max_allocation_per_star = allocation_data['adjusted_max_allocation_per_star'] + adjusted_max_allocation_per_galaxy = allocation_data['adjusted_max_allocation_per_galaxy'] + total_rounding_error_stars = allocation_data['total_rounding_error_stars'] + total_rounding_error_galaxies = allocation_data['total_rounding_error_galaxies'] + + # Calculate penalty pools + star_penalty_pool = Decimal('0') + for years in [1, 2, 3, 4]: + penalty = PENALTY_RATES[years] + star_penalty_pool += adjusted_max_allocation_per_star * stars_counts[years] * penalty + + galaxy_penalty_pool = Decimal('0') + for years in [1, 2, 3, 4]: + penalty = PENALTY_RATES[years] + galaxy_penalty_pool += adjusted_max_allocation_per_galaxy * galaxies_counts[years] * penalty + + star_penalty_pool = star_penalty_pool.quantize(TOKEN_PRECISION, rounding=ROUND_DOWN) + galaxy_penalty_pool = galaxy_penalty_pool.quantize(TOKEN_PRECISION, rounding=ROUND_DOWN) + + # Add rounding errors to bonus pools + star_bonus_pool_total = star_penalty_pool + total_rounding_error_stars + galaxy_bonus_pool_total = galaxy_penalty_pool + total_rounding_error_galaxies + + # Calculate bonus per 5-year participant + stars_5_years = stars_counts[5] + galaxies_5_years = galaxies_counts[5] + + bonus_per_star_5_years = star_bonus_pool_total / stars_5_years if stars_5_years > 0 else Decimal('0') + bonus_per_galaxy_5_years = galaxy_bonus_pool_total / galaxies_5_years if galaxies_5_years > 0 else Decimal('0') + + return { + 'star_penalty_pool': star_penalty_pool, + 'galaxy_penalty_pool': galaxy_penalty_pool, + 'star_bonus_pool_total': star_bonus_pool_total, + 'galaxy_bonus_pool_total': galaxy_bonus_pool_total, + 'bonus_per_star_5_years': bonus_per_star_5_years, + 'bonus_per_galaxy_5_years': bonus_per_galaxy_5_years + } + + +def calculate_final_allocations(allocation_data, bonus_data): + """Calculate final allocations for all lock periods.""" + adjusted_max_allocation_per_star = allocation_data['adjusted_max_allocation_per_star'] + adjusted_max_allocation_per_galaxy = allocation_data['adjusted_max_allocation_per_galaxy'] + bonus_per_star_5_years = bonus_data['bonus_per_star_5_years'] + bonus_per_galaxy_5_years = bonus_data['bonus_per_galaxy_5_years'] + stars_counts = allocation_data['stars_counts'] + galaxies_counts = allocation_data['galaxies_counts'] + + # Calculate final allocations + final_star_allocations = {} + final_galaxy_allocations = {} + + for years in range(1, 6): + penalty = PENALTY_RATES[years] + + if years == 5: + # 5-year participants get base + bonus + final_star_allocations[years] = (adjusted_max_allocation_per_star + bonus_per_star_5_years).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN) + final_galaxy_allocations[years] = (adjusted_max_allocation_per_galaxy + bonus_per_galaxy_5_years).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN) + else: + # Other years get penalized amounts + final_star_allocations[years] = (adjusted_max_allocation_per_star * (1 - penalty)).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN) + final_galaxy_allocations[years] = (adjusted_max_allocation_per_galaxy * (1 - penalty)).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN) + + # Calculate Z per block for each lock period + star_z_per_block = {} + galaxy_z_per_block = {} + + for years in range(1, 6): + lock_duration_fraction = Decimal(years) / Decimal('5') # Fraction of full 5-year period + effective_blocks = LOCKDROP_DURATION_BLOCKS * lock_duration_fraction + + star_z_per_block[years] = (final_star_allocations[years] / effective_blocks).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN) + galaxy_z_per_block[years] = (final_galaxy_allocations[years] / effective_blocks).quantize(TOKEN_PRECISION, rounding=ROUND_DOWN) + + # Calculate total allocations for verification + total_stars_allocation = sum(final_star_allocations[year] * stars_counts[year] for year in range(1, 6)) + total_galaxies_allocation = sum(final_galaxy_allocations[year] * galaxies_counts[year] for year in range(1, 6)) + + return { + 'final_star_allocations': final_star_allocations, + 'final_galaxy_allocations': final_galaxy_allocations, + 'star_z_per_block': star_z_per_block, + 'galaxy_z_per_block': galaxy_z_per_block, + 'total_stars_allocation': total_stars_allocation, + 'total_galaxies_allocation': total_galaxies_allocation + } + + +def generate_test_output(final_data): + """Generate JSON output for test validation.""" + final_star_allocations = final_data['final_star_allocations'] + final_galaxy_allocations = final_data['final_galaxy_allocations'] + total_allocation = final_data['total_stars_allocation'] + final_data['total_galaxies_allocation'] + + # Convert to $sZ units (multiply by 10^8) + output = { + "stars": { + f"{year}_years": int(final_star_allocations[year] * Decimal('1e8')) + for year in range(1, 6) + }, + "galaxies": { + f"{year}_years": int(final_galaxy_allocations[year] * Decimal('1e8')) + for year in range(1, 6) + }, + "total": int(total_allocation * Decimal('1e8')) + } + + return output + + +def print_analysis_tables(allocation_data, bonus_data, final_data): + """Print comprehensive analysis tables.""" + 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'] + 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'] + + print("=" * 80) + print("๐Ÿ“ˆ DYNAMIC LOCKDROP ANALYSIS") + print("=" * 80) + + # Participation rates + star_participation_rate = total_stars_locked / NUM_STARS + galaxy_participation_rate = total_galaxies_locked / NUM_GALAXIES + + print(f"\n๐Ÿ“Š PARTICIPATION RATES") + print(f" Stars: {star_participation_rate:.1%} ({total_stars_locked:,}/{NUM_STARS:,})") + print(f" Galaxies: {galaxy_participation_rate:.1%} ({total_galaxies_locked:,}/{NUM_GALAXIES:,})") + + # Final allocations table + print("\nโญ FINAL STAR ALLOCATIONS") + 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]:.12f}" for year in [5, 4, 3, 2, 1]], + 'Z per Block': [f"{star_z_per_block[year]:.12f}" for year in [5, 4, 3, 2, 1]], + 'Participants': [f"{stars_counts[year]:,}" for year in [5, 4, 3, 2, 1]] + }) + print(star_final_df.to_string(index=False)) + + print("\n๐ŸŒŒ FINAL GALAXY ALLOCATIONS") + 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]:.12f}" for year in [5, 4, 3, 2, 1]], + 'Z per Block': [f"{galaxy_z_per_block[year]:.12f}" for year in [5, 4, 3, 2, 1]], + 'Participants': [f"{galaxies_counts[year]:,}" for year in [5, 4, 3, 2, 1]] + }) + print(galaxy_final_df.to_string(index=False)) + + # Bonus pool summary + print("\n๐ŸŽ BONUS POOL SUMMARY") + bonus_summary_df = pd.DataFrame({ + 'Component': ['Star Bonus Pool', 'Galaxy Bonus Pool', 'Total Bonus Distributed'], + 'Value': [ + f"{bonus_data['star_bonus_pool_total']:,.6f} $Z", + f"{bonus_data['galaxy_bonus_pool_total']:,.6f} $Z", + f"{bonus_data['star_bonus_pool_total'] + bonus_data['galaxy_bonus_pool_total']:,.6f} $Z" + ] + }) + print(bonus_summary_df.to_string(index=False)) + + print("\n" + "="*80) + + +def create_visualization(allocation_data, final_data): + """Create comprehensive visualization plots.""" + stars_counts = allocation_data['stars_counts'] + galaxies_counts = allocation_data['galaxies_counts'] + final_star_allocations = final_data['final_star_allocations'] + final_galaxy_allocations = final_data['final_galaxy_allocations'] + + fig = plt.figure(figsize=(20, 10)) + gs = fig.add_gridspec(2, 3, hspace=0.35, wspace=0.35) + + fig.suptitle('Lockdrop Analysis', fontsize=18, fontweight='bold', y=0.98) + + # 1. Star Participation Distribution + ax1 = fig.add_subplot(gs[0, 0]) + star_years = [1, 2, 3, 4, 5] + star_counts = [stars_counts[year] for year in star_years] + + bars1 = ax1.bar(range(len(star_years)), star_counts, + color=['#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728'], alpha=0.8) + ax1.set_xlabel('Lock Period (Years)') + ax1.set_ylabel('Number of Stars') + ax1.set_title('Star Participation by Lock Period', fontweight='bold') + ax1.set_xticks(range(len(star_years))) + ax1.set_xticklabels(star_years) + + for i, count in enumerate(star_counts): + height = bars1[i].get_height() + ax1.text(bars1[i].get_x() + bars1[i].get_width()/2., height + height*0.01, + f'{count:,}', ha='center', va='bottom', fontweight='bold', fontsize=9) + + # 2. Galaxy Participation Distribution + ax2 = fig.add_subplot(gs[0, 1]) + galaxy_counts = [galaxies_counts[year] for year in star_years] + + bars2 = ax2.bar(range(len(star_years)), galaxy_counts, + color=['#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2'], alpha=0.8) + ax2.set_xlabel('Lock Period (Years)') + ax2.set_ylabel('Number of Galaxies') + ax2.set_title('Galaxy Participation by Lock Period', fontweight='bold') + ax2.set_xticks(range(len(star_years))) + ax2.set_xticklabels(star_years) + + for i, count in enumerate(galaxy_counts): + height = bars2[i].get_height() + ax2.text(bars2[i].get_x() + bars2[i].get_width()/2., height + height*0.01, + f'{count:,}', ha='center', va='bottom', fontweight='bold', fontsize=9) + + # 3. Star Allocations + ax3 = fig.add_subplot(gs[0, 2]) + star_alloc_values = [float(final_star_allocations[year]) for year in star_years] + + bars3 = ax3.bar(range(len(star_years)), star_alloc_values, + color=['#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728'], alpha=0.8) + ax3.set_xlabel('Lock Period (Years)') + ax3.set_ylabel('Allocation per Star ($Z)') + ax3.set_title('Star Allocations by Lock Period', fontweight='bold') + ax3.set_xticks(range(len(star_years))) + ax3.set_xticklabels(star_years) + + for i, allocation in enumerate(star_alloc_values): + height = bars3[i].get_height() + ax3.text(bars3[i].get_x() + bars3[i].get_width()/2., height + height*0.01, + f'{allocation:,.0f}', ha='center', va='bottom', fontweight='bold', fontsize=9) + + # 4. Galaxy Allocations + ax4 = fig.add_subplot(gs[1, 0]) + galaxy_alloc_values = [float(final_galaxy_allocations[year]) for year in star_years] + + bars4 = ax4.bar(range(len(star_years)), galaxy_alloc_values, + color=['#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2'], alpha=0.8) + ax4.set_xlabel('Lock Period (Years)') + ax4.set_ylabel('Allocation per Galaxy ($Z)') + ax4.set_title('Galaxy Allocations by Lock Period', fontweight='bold') + ax4.set_xticks(range(len(star_years))) + ax4.set_xticklabels(star_years) + + for i, allocation in enumerate(galaxy_alloc_values): + height = bars4[i].get_height() + ax4.text(bars4[i].get_x() + bars4[i].get_width()/2., height + height*0.01, + f'{allocation:,.0f}', ha='center', va='bottom', fontweight='bold', fontsize=9) + + # Hide remaining subplots + ax5 = fig.add_subplot(gs[1, 1]) + ax5.axis('off') + ax6 = fig.add_subplot(gs[1, 2]) + ax6.axis('off') + + plt.subplots_adjust(bottom=0.08, top=0.88, left=0.05, right=0.98) + plt.show() + + +def load_watcher_events_data(generated_dir='generated'): + """Load participation data from watcher events file.""" + import urbitob + + watcher_events_path = os.path.join(generated_dir, 'watcher-events.json') + + try: + with open(watcher_events_path, 'r') as f: + data = json.load(f) + events = data['data']['eventsInRange'] + + # Analyze events + lock_duration_counts = { + 'star': defaultdict(int), + 'galaxy': defaultdict(int) + } + + for event_data in events: + if event_data['event']['__typename'] == 'PointLockedEvent': + point = event_data['event']['point'] + lock_period = event_data['event']['lock_period'] + + point_num = urbitob.patp_to_num(point) + point_type = "galaxy" if point_num < NUM_GALAXIES else "star" + lock_duration_counts[point_type][lock_period] += 1 + + # Extract counts + result = {} + for years in [1, 2, 3, 4, 5]: + result[f'stars_{years}_years'] = lock_duration_counts['star'][years] + result[f'galaxies_{years}_years'] = lock_duration_counts['galaxy'][years] + + return result, True + + except FileNotFoundError: + print(f"โš ๏ธ Watcher events file not found at {watcher_events_path}") + return None, False + except Exception as e: + print(f"โš ๏ธ Error loading watcher events: {e}") + return None, False diff --git a/requirements.txt b/requirements.txt index d4c2076..121e0cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ numpy urbitob tabulate requests +ipywidgets