Aztec Prover Rewards Simulator
  • Simulator

On this page

  • Aztec RewardBooster Miss-Penalty Simulator
    • Model assumptions
    • Core simulation functions
    • Static comparison: one missed epoch with default economics
    • Interactive comparison dashboard
    • Interactive single-parameter-set simulator
    • Gas and infrastructure sensitivity tables

  • Show All Code
  • Hide All Code

Aztec RewardBooster Miss-Penalty Simulator

This notebook provides an interactive simulation for three RewardBooster parameter sets:

  1. Current / original
  2. AZIP-5 proposed
  3. htimsk softer revision

It models what happens when a prover misses one or more epochs, how long it takes to return to full reward weight, the reward opportunity cost, the gross cash required to fund the recovery proofs, and the profitability line under configurable market and cost assumptions.

The notebook is intentionally self-contained. Change the controls in the interactive sections to test different ETH/USD, gas price, AZTC/USD, infrastructure cost, gas units per proof, active prover count, reward pool, and parameter values.

Model assumptions

The simulator uses the following mechanics:

score_after_success = min(maxScore, max(0, priorScore - decayRate * elapsedEpochs) + increment)
share = max(k - a * (maxScore - score)^2 / 1e10, minimum)

For a prover that was previously at maxScore, misses m consecutive epochs, and then submits a valid proof, the elapsed interval into the comeback proof is modeled as m + 1 epochs. This matches the usual interpretation that a prover proves epoch E, misses epochs E+1 ... E+m, and returns at epoch E+m+1.

Economic model:

gas_cost_usd = gas_units_per_proof * gas_price_gwei * 1e-9 * ETH_USD
cash_cost_per_proof = gas_cost_usd + infrastructure_cost_per_proof
reward_fraction = prover_share / (peer_full_weight_shares + prover_share)
reward_usd = epoch_reward_pool_AZTC * AZTC_USD * reward_fraction
profit_usd = reward_usd - cash_cost_per_proof

Important interpretation:

  • Reward opportunity cost is the missed and reduced rewards relative to being a full-weight prover.
  • Gross recovery cash spend is the cash needed to fund proofs until the first full-weight proof is reached.
  • For a committed prover who would have continued proving anyway, the incremental economic burden is mostly reward opportunity cost. For an opportunistic prover, the gross cash needed to re-establish full weight is also important.
Code

from __future__ import annotations

from dataclasses import dataclass, asdict
from typing import Dict, Tuple

import json
import math
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import plotly.offline as plotly_offline
from IPython.display import display, Markdown, HTML

PLOTLY_CDN_URL = f"https://cdn.plot.ly/plotly-{plotly_offline.get_plotlyjs_version()}.min.js"

pd.set_option("display.max_columns", 100)
pd.set_option("display.max_rows", 200)
pd.set_option("display.float_format", lambda x: f"{x:,.6f}")
Code

@dataclass(frozen=True)
class RewardBoosterParams:
    name: str
    increment: float
    max_score: float
    a: float
    minimum: float
    k: float = 1_000_000.0
    decay_rate: float = 100_000.0


PARAMETER_PRESETS: Dict[str, RewardBoosterParams] = {
    "Current / original": RewardBoosterParams(
        name="Current / original",
        increment=125_000,
        max_score=15_000_000,
        a=1_000,
        minimum=100_000,
        k=1_000_000,
        decay_rate=100_000,
    ),
    "AZIP-5 proposed": RewardBoosterParams(
        name="AZIP-5 proposed",
        increment=101_400,
        max_score=367_500,
        a=250_000,
        minimum=10_000,
        k=1_000_000,
        decay_rate=100_000,
    ),
    "htimsk softer revision": RewardBoosterParams(
        name="htimsk softer revision",
        increment=110_000,
        max_score=367_500,
        a=100_000,
        minimum=10_000,
        k=1_000_000,
        decay_rate=100_000,
    ),
}

DEFAULT_GAS_UNITS_PER_PROOF = 3_850_000  # Replace with a fresher chain estimate if desired.
DEFAULT_INFRA_COST_PER_PROOF_USD = 44.0
DEFAULT_ETH_USD = 2_200.0
DEFAULT_GAS_PRICE_GWEI = 2.0
DEFAULT_AZTC_USD = 0.02
DEFAULT_EPOCH_REWARD_POOL_AZTC = 4_800.0
DEFAULT_ACTIVE_FULL_WEIGHT_PROVERS = 10
DEFAULT_EPOCH_MINUTES = 38

pd.DataFrame([asdict(p) for p in PARAMETER_PRESETS.values()])
name increment max_score a minimum k decay_rate
0 Current / original 125000 15000000 1000 100000 1000000 100000
1 AZIP-5 proposed 101400 367500 250000 10000 1000000 100000
2 htimsk softer revision 110000 367500 100000 10000 1000000 100000

Core simulation functions

These functions are used by both the static tables and the interactive dashboard. You can reuse them in your own analysis cells.

Code

def gas_cost_usd(
    gas_units_per_proof: float,
    gas_price_gwei: float,
    eth_usd: float,
) -> float:
    """Return gas cost per proof in USD."""
    return gas_units_per_proof * gas_price_gwei * 1e-9 * eth_usd


def cash_cost_per_proof_usd(
    gas_units_per_proof: float,
    gas_price_gwei: float,
    eth_usd: float,
    infra_cost_per_proof_usd: float,
) -> float:
    return gas_cost_usd(gas_units_per_proof, gas_price_gwei, eth_usd) + infra_cost_per_proof_usd


def maintenance_coverage(params: RewardBoosterParams) -> float:
    """Long-run minimum coverage required for expected score drift >= 0."""
    if params.increment <= 0:
        return math.inf
    return min(1.0, params.decay_rate / params.increment)


def share_from_score(score: float | np.ndarray, params: RewardBoosterParams) -> float | np.ndarray:
    """Map score to RewardBooster share, with score capped at maxScore for modeling."""
    score_arr = np.asarray(score, dtype=float)
    capped_score = np.minimum(np.maximum(score_arr, 0), params.max_score)
    raw_share = params.k - params.a * (params.max_score - capped_score) ** 2 / 1e10
    shares = np.maximum(raw_share, params.minimum)
    if np.ndim(score) == 0:
        return float(shares)
    return shares


def successful_update(prior_score: float, elapsed_epochs: int, params: RewardBoosterParams) -> float:
    """Apply decay over elapsed epochs, add increment for a successful proof, and cap at maxScore."""
    decayed = max(0.0, prior_score - params.decay_rate * elapsed_epochs)
    return min(params.max_score, decayed + params.increment)


def reward_fraction_of_epoch_pool(
    prover_share: float,
    params: RewardBoosterParams,
    active_full_weight_provers: int,
) -> float:
    """
    Return the fraction of the epoch reward pool paid to this prover.

    active_full_weight_provers is the number of provers in the peer set if this prover were healthy,
    including this prover. Other provers are modeled as full weight.
    """
    active_full_weight_provers = max(int(active_full_weight_provers), 1)
    peer_count = max(active_full_weight_provers - 1, 0)
    denominator = peer_count * params.k + prover_share
    if denominator <= 0:
        return 0.0
    return prover_share / denominator


def healthy_reward_fraction(params: RewardBoosterParams, active_full_weight_provers: int) -> float:
    """The prover's reward fraction at full weight, assuming equal full-weight peers."""
    active_full_weight_provers = max(int(active_full_weight_provers), 1)
    return 1.0 / active_full_weight_provers


def recovery_dataframe(
    params: RewardBoosterParams,
    missed_epochs: int = 1,
    start_score: float | None = None,
    max_successful_proofs: int = 50_000,
) -> pd.DataFrame:
    """
    Simulate the path after missing `missed_epochs` consecutive epochs from `start_score`.

    Returns rows for missed epochs and subsequent successful proof submissions until the first full-weight proof.
    """
    if missed_epochs < 0:
        raise ValueError("missed_epochs must be non-negative")
    if start_score is None:
        start_score = params.max_score

    rows = []

    # Missed epochs: no proof, no reward, no cash spend modeled.
    for i in range(1, missed_epochs + 1):
        rows.append({
            "event": "missed_epoch",
            "timeline_epoch": i,
            "proof_index_after_return": np.nan,
            "elapsed_epochs_into_update": np.nan,
            "score": np.nan,
            "share": 0.0,
            "relative_weight": 0.0,
            "at_full_weight": False,
            "raw_weight_loss_eq": 1.0,
        })

    first_elapsed = 1 if missed_epochs == 0 else missed_epochs + 1
    score = successful_update(start_score, first_elapsed, params)

    for proof_idx in range(1, max_successful_proofs + 1):
        share = share_from_score(score, params)
        at_full_weight = score >= params.max_score - 1e-9
        rows.append({
            "event": "recovery_proof",
            "timeline_epoch": missed_epochs + proof_idx,
            "proof_index_after_return": proof_idx,
            "elapsed_epochs_into_update": first_elapsed if proof_idx == 1 else 1,
            "score": score,
            "share": share,
            "relative_weight": share / params.k,
            "at_full_weight": at_full_weight,
            "raw_weight_loss_eq": max(0.0, 1.0 - share / params.k),
        })
        if at_full_weight:
            break
        score = successful_update(score, 1, params)
    else:
        raise RuntimeError("Recovery did not reach full weight before max_successful_proofs.")

    return pd.DataFrame(rows)


def add_economics(
    df: pd.DataFrame,
    params: RewardBoosterParams,
    eth_usd: float = DEFAULT_ETH_USD,
    gas_price_gwei: float = DEFAULT_GAS_PRICE_GWEI,
    aztc_usd: float = DEFAULT_AZTC_USD,
    infra_cost_per_proof_usd: float = DEFAULT_INFRA_COST_PER_PROOF_USD,
    gas_units_per_proof: float = DEFAULT_GAS_UNITS_PER_PROOF,
    epoch_reward_pool_aztc: float = DEFAULT_EPOCH_REWARD_POOL_AZTC,
    active_full_weight_provers: int = DEFAULT_ACTIVE_FULL_WEIGHT_PROVERS,
) -> pd.DataFrame:
    """Add USD-denominated cost/revenue/profit columns to a recovery dataframe."""
    out = df.copy()
    gas_usd = gas_cost_usd(gas_units_per_proof, gas_price_gwei, eth_usd)
    cash_usd = gas_usd + infra_cost_per_proof_usd
    epoch_pool_usd = epoch_reward_pool_aztc * aztc_usd
    healthy_fraction = healthy_reward_fraction(params, active_full_weight_provers)
    healthy_reward_usd = epoch_pool_usd * healthy_fraction

    reward_fractions = []
    reward_usd_values = []
    cash_spend_values = []
    gas_spend_values = []
    infra_spend_values = []
    profit_values = []
    opportunity_loss_usd_values = []
    exact_opportunity_loss_eq_values = []

    for _, row in out.iterrows():
        if row["event"] == "missed_epoch":
            reward_fraction = 0.0
            reward_usd = 0.0
            gas_spend = 0.0
            infra_spend = 0.0
            cash_spend = 0.0
            profit_usd = 0.0
            opportunity_loss_usd = healthy_reward_usd
        else:
            reward_fraction = reward_fraction_of_epoch_pool(row["share"], params, active_full_weight_provers)
            reward_usd = epoch_pool_usd * reward_fraction
            gas_spend = gas_usd
            infra_spend = infra_cost_per_proof_usd
            cash_spend = cash_usd
            profit_usd = reward_usd - cash_spend
            opportunity_loss_usd = max(0.0, healthy_reward_usd - reward_usd)

        reward_fractions.append(reward_fraction)
        reward_usd_values.append(reward_usd)
        gas_spend_values.append(gas_spend)
        infra_spend_values.append(infra_spend)
        cash_spend_values.append(cash_spend)
        profit_values.append(profit_usd)
        opportunity_loss_usd_values.append(opportunity_loss_usd)
        exact_opportunity_loss_eq_values.append(
            opportunity_loss_usd / healthy_reward_usd if healthy_reward_usd > 0 else np.nan
        )

    out["reward_fraction_of_pool"] = reward_fractions
    out["reward_usd"] = reward_usd_values
    out["gas_spend_usd"] = gas_spend_values
    out["infra_spend_usd"] = infra_spend_values
    out["cash_spend_usd"] = cash_spend_values
    out["profit_usd"] = profit_values
    out["reward_opportunity_loss_usd"] = opportunity_loss_usd_values
    out["exact_opportunity_loss_eq"] = exact_opportunity_loss_eq_values
    out["cumulative_cash_spend_usd"] = out["cash_spend_usd"].cumsum()
    out["cumulative_reward_opportunity_loss_usd"] = out["reward_opportunity_loss_usd"].cumsum()
    out["cumulative_raw_weight_loss_eq"] = out["raw_weight_loss_eq"].cumsum()
    out["cumulative_exact_opportunity_loss_eq"] = out["exact_opportunity_loss_eq"].cumsum()
    return out


def break_even_share(
    params: RewardBoosterParams,
    cash_cost_usd_value: float,
    epoch_reward_pool_aztc: float,
    aztc_usd: float,
    active_full_weight_provers: int,
) -> Tuple[float, float, str]:
    """
    Return the share required to break even, relative weight, and a status string.

    The break-even equation is:
        epoch_pool_usd * share / ((N-1)*k + share) = cash_cost_usd
    """
    epoch_pool_usd = epoch_reward_pool_aztc * aztc_usd
    if epoch_pool_usd <= 0:
        return np.inf, np.inf, "No rewards: AZTC price or reward pool is zero."

    active_full_weight_provers = max(int(active_full_weight_provers), 1)
    peers = max(active_full_weight_provers - 1, 0)
    required_fraction = cash_cost_usd_value / epoch_pool_usd
    max_fraction = 1.0 / active_full_weight_provers

    if required_fraction <= 0:
        return 0.0, 0.0, "Profitable at any positive reward share."
    if required_fraction > max_fraction + 1e-15:
        return np.inf, np.inf, "Not profitable even at full weight."
    if peers == 0:
        return 0.0, 0.0, "Single-prover case: any positive share receives the whole pool."

    # share / (peers*k + share) = required_fraction
    share_required = (required_fraction * peers * params.k) / (1.0 - required_fraction)
    relative_weight_required = share_required / params.k
    return share_required, relative_weight_required, "Break-even share found."


def score_for_share(target_share: float, params: RewardBoosterParams) -> float:
    """Invert the share function to estimate the minimum score needed for `target_share`."""
    if not np.isfinite(target_share):
        return np.inf
    target_share = max(float(target_share), params.minimum)
    if target_share <= params.minimum:
        return 0.0
    if target_share >= params.k:
        return params.max_score
    delta = math.sqrt(max(0.0, (params.k - target_share) * 1e10 / params.a))
    return max(0.0, params.max_score - delta)


def break_even_aztc_price_at_full_weight(
    cash_cost_usd_value: float,
    epoch_reward_pool_aztc: float,
    active_full_weight_provers: int,
) -> float:
    """AZTC/USD needed for a full-weight prover to break even per proof."""
    if epoch_reward_pool_aztc <= 0:
        return np.inf
    return cash_cost_usd_value * max(int(active_full_weight_provers), 1) / epoch_reward_pool_aztc


def break_even_gas_price_at_full_weight_gwei(
    eth_usd: float,
    aztc_usd: float,
    epoch_reward_pool_aztc: float,
    active_full_weight_provers: int,
    gas_units_per_proof: float,
    infra_cost_per_proof_usd: float,
) -> float:
    """Gas price in gwei at which a full-weight proof breaks even."""
    reward_usd_full_weight = epoch_reward_pool_aztc * aztc_usd / max(int(active_full_weight_provers), 1)
    remaining_for_gas = reward_usd_full_weight - infra_cost_per_proof_usd
    if remaining_for_gas < 0 or eth_usd <= 0 or gas_units_per_proof <= 0:
        return -np.inf
    return remaining_for_gas / (gas_units_per_proof * 1e-9 * eth_usd)


def summarize_scenario(
    params: RewardBoosterParams,
    missed_epochs: int = 1,
    eth_usd: float = DEFAULT_ETH_USD,
    gas_price_gwei: float = DEFAULT_GAS_PRICE_GWEI,
    aztc_usd: float = DEFAULT_AZTC_USD,
    infra_cost_per_proof_usd: float = DEFAULT_INFRA_COST_PER_PROOF_USD,
    gas_units_per_proof: float = DEFAULT_GAS_UNITS_PER_PROOF,
    epoch_reward_pool_aztc: float = DEFAULT_EPOCH_REWARD_POOL_AZTC,
    active_full_weight_provers: int = DEFAULT_ACTIVE_FULL_WEIGHT_PROVERS,
) -> dict:
    """Return a compact summary of a miss/recovery scenario."""
    df = recovery_dataframe(params, missed_epochs=missed_epochs)
    econ = add_economics(
        df,
        params,
        eth_usd=eth_usd,
        gas_price_gwei=gas_price_gwei,
        aztc_usd=aztc_usd,
        infra_cost_per_proof_usd=infra_cost_per_proof_usd,
        gas_units_per_proof=gas_units_per_proof,
        epoch_reward_pool_aztc=epoch_reward_pool_aztc,
        active_full_weight_provers=active_full_weight_provers,
    )
    proofs = econ[econ["event"] == "recovery_proof"].copy()
    first_proof = proofs.iloc[0]
    underweight_proofs = int((proofs["share"] < params.k - 1e-9).sum())
    recovery_proofs = int(len(proofs))
    cash_per_proof = cash_cost_per_proof_usd(
        gas_units_per_proof, gas_price_gwei, eth_usd, infra_cost_per_proof_usd
    )
    share_required, rel_required, breakeven_status = break_even_share(
        params,
        cash_per_proof,
        epoch_reward_pool_aztc,
        aztc_usd,
        active_full_weight_provers,
    )
    score_required = score_for_share(share_required, params)

    return {
        "parameter_set": params.name,
        "missed_epochs": missed_epochs,
        "increment": params.increment,
        "maxScore": params.max_score,
        "a": params.a,
        "minimum": params.minimum,
        "maintenance_coverage_%": 100 * maintenance_coverage(params),
        "max_miss_rate_without_score_decay_%": 100 * max(0.0, 1.0 - maintenance_coverage(params)),
        "first_return_score": float(first_proof["score"]),
        "first_return_share_%": 100 * float(first_proof["share"] / params.k),
        "underweight_recovery_proofs": underweight_proofs,
        "recovery_proofs_to_full_weight": recovery_proofs,
        "recovery_time_hours": recovery_proofs * DEFAULT_EPOCH_MINUTES / 60,
        "raw_reward_weight_loss_eq": float(econ["raw_weight_loss_eq"].sum()),
        "exact_reward_opportunity_loss_eq": float(econ["exact_opportunity_loss_eq"].sum()),
        "reward_opportunity_loss_usd": float(econ["reward_opportunity_loss_usd"].sum()),
        "gross_recovery_cash_spend_usd": float(proofs["cash_spend_usd"].sum()),
        "gas_cost_per_proof_usd": gas_cost_usd(gas_units_per_proof, gas_price_gwei, eth_usd),
        "cash_cost_per_proof_usd": cash_per_proof,
        "full_weight_reward_per_proof_usd": epoch_reward_pool_aztc * aztc_usd / max(active_full_weight_provers, 1),
        "full_weight_profit_per_proof_usd": epoch_reward_pool_aztc * aztc_usd / max(active_full_weight_provers, 1) - cash_per_proof,
        "break_even_relative_weight_%": 100 * rel_required if np.isfinite(rel_required) else np.inf,
        "break_even_score": score_required,
        "break_even_aztc_usd_at_full_weight": break_even_aztc_price_at_full_weight(
            cash_per_proof, epoch_reward_pool_aztc, active_full_weight_provers
        ),
        "break_even_gas_gwei_at_full_weight": break_even_gas_price_at_full_weight_gwei(
            eth_usd,
            aztc_usd,
            epoch_reward_pool_aztc,
            active_full_weight_provers,
            gas_units_per_proof,
            infra_cost_per_proof_usd,
        ),
        "break_even_status": breakeven_status,
    }


def compare_presets_table(
    missed_epochs: int = 1,
    eth_usd: float = DEFAULT_ETH_USD,
    gas_price_gwei: float = DEFAULT_GAS_PRICE_GWEI,
    aztc_usd: float = DEFAULT_AZTC_USD,
    infra_cost_per_proof_usd: float = DEFAULT_INFRA_COST_PER_PROOF_USD,
    gas_units_per_proof: float = DEFAULT_GAS_UNITS_PER_PROOF,
    epoch_reward_pool_aztc: float = DEFAULT_EPOCH_REWARD_POOL_AZTC,
    active_full_weight_provers: int = DEFAULT_ACTIVE_FULL_WEIGHT_PROVERS,
) -> pd.DataFrame:
    rows = [
        summarize_scenario(
            p,
            missed_epochs=missed_epochs,
            eth_usd=eth_usd,
            gas_price_gwei=gas_price_gwei,
            aztc_usd=aztc_usd,
            infra_cost_per_proof_usd=infra_cost_per_proof_usd,
            gas_units_per_proof=gas_units_per_proof,
            epoch_reward_pool_aztc=epoch_reward_pool_aztc,
            active_full_weight_provers=active_full_weight_provers,
        )
        for p in PARAMETER_PRESETS.values()
    ]
    return pd.DataFrame(rows)

Static comparison: one missed epoch with default economics

The defaults below are illustrative and should be overwritten with current network values when doing production analysis.

Code

def format_summary_table(df: pd.DataFrame) -> pd.DataFrame:
    keep = [
        "parameter_set",
        "maintenance_coverage_%",
        "first_return_share_%",
        "underweight_recovery_proofs",
        "recovery_proofs_to_full_weight",
        "recovery_time_hours",
        "raw_reward_weight_loss_eq",
        "reward_opportunity_loss_usd",
        "gross_recovery_cash_spend_usd",
        "cash_cost_per_proof_usd",
        "full_weight_profit_per_proof_usd",
        "break_even_relative_weight_%",
        "break_even_aztc_usd_at_full_weight",
        "break_even_gas_gwei_at_full_weight",
        "break_even_status",
    ]
    return df[keep].copy()

static_summary = compare_presets_table(missed_epochs=1)
format_summary_table(static_summary)
parameter_set maintenance_coverage_% first_return_share_% underweight_recovery_proofs recovery_proofs_to_full_weight recovery_time_hours raw_reward_weight_loss_eq reward_opportunity_loss_usd gross_recovery_cash_spend_usd cash_cost_per_proof_usd full_weight_profit_per_proof_usd break_even_relative_weight_% break_even_aztc_usd_at_full_weight break_even_gas_gwei_at_full_weight break_even_status
0 Current / original 80.000000 99.943750 3 4 2.533333 1.000875 9.607560 243.760000 60.940000 -51.340000 inf 0.126958 -inf Not profitable even at full weight.
1 AZIP-5 proposed 98.619329 75.695100 71 72 45.600000 6.827964 60.711656 4,387.680000 60.940000 -51.340000 inf 0.126958 -inf Not profitable even at full weight.
2 htimsk softer revision 90.909091 91.900000 9 10 6.333333 1.285000 12.075733 609.400000 60.940000 -51.340000 inf 0.126958 -inf Not profitable even at full weight.

Interactive comparison dashboard

Use this dashboard to compare the three parameter sets side-by-side under the same economic assumptions.

The summary table includes:

  • raw_reward_weight_loss_eq: missed and reduced share in simple max-weight epoch equivalents.
  • reward_opportunity_loss_usd: exact pro-rata reward opportunity cost under the configured active prover count.
  • gross_recovery_cash_spend_usd: gross cash required to fund all recovery proofs until the first full-weight proof.
  • break_even_relative_weight_%: minimum relative weight required for a proof to break even under the configured economics.
Code

def _script_safe_json(data) -> str:
    return json.dumps(data, allow_nan=False).replace("</", "<\\/")


PLOTLY_PRESETS = {name: asdict(params) for name, params in PARAMETER_PRESETS.items()}
PLOTLY_DEFAULTS = {
    "gas_units_per_proof": DEFAULT_GAS_UNITS_PER_PROOF,
    "infra_cost_per_proof_usd": DEFAULT_INFRA_COST_PER_PROOF_USD,
    "eth_usd": DEFAULT_ETH_USD,
    "gas_price_gwei": DEFAULT_GAS_PRICE_GWEI,
    "aztc_usd": DEFAULT_AZTC_USD,
    "epoch_reward_pool_aztc": DEFAULT_EPOCH_REWARD_POOL_AZTC,
    "active_full_weight_provers": DEFAULT_ACTIVE_FULL_WEIGHT_PROVERS,
    "epoch_minutes": DEFAULT_EPOCH_MINUTES,
}


def render_preset_comparison_plotly_controls():
    display(Markdown("### Side-by-side recovery and cost summary"))
    html = """
<div class="aztec-plotly-widget" id="aztec-comparison-widget">
  <div class="aztec-control-grid">
    <label>Missed epochs <input id="aztec-comparison-missed" type="range" min="1" max="20" step="1" value="1" /></label>
    <output id="aztec-comparison-missed-value">1</output>
    <label>Active provers <input id="aztec-comparison-active-provers" type="number" min="1" step="1" /></label>
    <label>Epoch reward AZTC <input id="aztec-comparison-reward-pool" type="number" min="0" step="100" /></label>
    <label>AZTC/USD <input id="aztec-comparison-aztc-usd" type="number" min="0" step="0.001" /></label>
    <label>ETH/USD <input id="aztec-comparison-eth-usd" type="number" min="0" step="100" /></label>
    <label>Gas gwei <input id="aztec-comparison-gas-gwei" type="number" min="0" step="0.1" /></label>
    <label>Gas units/proof <input id="aztec-comparison-gas-units" type="number" min="0" step="1000" /></label>
    <label>Infra $/proof <input id="aztec-comparison-infra-cost" type="number" min="0" step="1" /></label>
  </div>
  <div id="aztec-comparison-summary" class="aztec-summary-table"></div>
  <div id="aztec-comparison-plot-status" class="aztec-plot-status"></div>
  <div id="aztec-comparison-plot-title" class="aztec-plot-title"></div>
  <div id="aztec-comparison-plot" class="aztec-panel-grid aztec-comparison-grid">
    <section class="aztec-panel"><h4>Recovery path</h4><div id="aztec-comparison-recovery-plot" class="aztec-panel-plot"></div><div id="aztec-comparison-recovery-legend" class="aztec-panel-legend"></div></section>
    <section class="aztec-panel"><h4>Gross cash spend<br>to reach full weight</h4><div id="aztec-comparison-cash-plot" class="aztec-panel-plot"></div><div id="aztec-comparison-cash-legend" class="aztec-panel-legend"></div></section>
    <section class="aztec-panel"><h4>Penalty cliff</h4><div id="aztec-comparison-penalty-plot" class="aztec-panel-plot"></div><div id="aztec-comparison-penalty-legend" class="aztec-panel-legend"></div></section>
    <section class="aztec-panel"><h4>Full-weight profitability</h4><div id="aztec-comparison-profit-plot" class="aztec-panel-plot"></div><div id="aztec-comparison-profit-legend" class="aztec-panel-legend"></div></section>
  </div>
</div>
<style>
  .aztec-plotly-widget { width: 100%; }
  .aztec-control-grid { align-items: end; display: grid; gap: 10px 12px; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); margin: 10px 0 14px; }
  .aztec-control-grid label { display: grid; font-size: 13px; font-weight: 600; gap: 3px; }
  .aztec-control-grid input, .aztec-control-grid select { box-sizing: border-box; min-width: 0; width: 100%; }
  .aztec-control-grid input[type="range"] { min-width: 150px; }
  .aztec-control-grid output { align-self: center; font-weight: 600; }
  .aztec-summary-table { overflow-x: auto; margin-bottom: 12px; }
  .aztec-summary-table table { border-collapse: collapse; font-size: 13px; min-width: 920px; width: 100%; }
  .aztec-summary-table th, .aztec-summary-table td { border: 1px solid #d1d5db; padding: 6px 8px; text-align: left; vertical-align: top; }
  .aztec-summary-table th { background: #f3f4f6; }
  .aztec-plot-status { color: #4b5563; font-size: 13px; margin: 6px 0; }
  .aztec-plot-title { font-size: 18px; font-weight: 700; margin: 10px 0 10px; text-align: center; }
  .aztec-panel-grid { display: grid; gap: 18px 22px; margin-top: 8px; }
  .aztec-comparison-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .aztec-panel { min-width: 0; }
  .aztec-panel h4 { font-size: 20px; font-weight: 700; line-height: 1.15; margin: 0 0 8px; min-height: 50px; text-align: center; }
  .aztec-panel-plot { height: 300px; width: 100%; }
  .aztec-panel-legend { align-items: center; display: flex; flex-wrap: wrap; gap: 4px 12px; justify-content: center; min-height: 20px; padding-top: 2px; }
  .aztec-legend-item { align-items: center; color: #374151; display: inline-flex; font-size: 12px; gap: 5px; line-height: 1.2; white-space: nowrap; }
  .aztec-legend-line { border-top: 2px solid var(--legend-color); display: inline-block; flex: 0 0 auto; width: 22px; }
  .aztec-legend-line.dash { border-top-style: dashed; }
  .aztec-legend-line.dot { border-top-style: dotted; }
  @media (max-width: 820px) { .aztec-comparison-grid { grid-template-columns: 1fr; } }
</style>
<script src="__PLOTLY_CDN__"></script>
<script>
(() => {
  const PRESETS = __PRESETS__;
  const DEFAULTS = __DEFAULTS__;
  const COLORS = {
    "Current / original": "#1f77b4",
    "AZIP-5 proposed": "#ff7f0e",
    "htimsk softer revision": "#2ca02c",
    "Custom": "#9467bd"
  };
  const comparisonColumns = ["Parameter set", "First return", "Recovery proofs", "Recovery time", "Reward loss", "Gross cash", "Break-even"];
  const summaryColumns = ["Metric", "Value"];
  const pathColumns = ["event", "timeline_epoch", "proof_index_after_return", "score", "relative_weight", "reward_usd", "cash_spend_usd", "profit_usd", "reward_opportunity_loss_usd", "cumulative_cash_spend_usd", "cumulative_reward_opportunity_loss_usd"];

  function byId(id) { return document.getElementById(id); }
  function num(id) { return Number(byId(id).value); }
  function setVal(id, value) { const el = byId(id); if (el) el.value = value; }
  function finite(value) { return Number.isFinite(value); }
  function linspace(start, stop, count) {
    if (count <= 1) return [start];
    const step = (stop - start) / (count - 1);
    return Array.from({length: count}, (_, i) => start + step * i);
  }
  function intRange(start, stopInclusive) {
    return Array.from({length: Math.max(0, stopInclusive - start + 1)}, (_, i) => start + i);
  }
  function fmtNumber(value, decimals = 2, prefix = "", suffix = "") {
    if (value === null || value === undefined || Number.isNaN(value)) return "";
    if (value === Infinity) return "inf";
    if (value === -Infinity) return "-inf";
    return `${prefix}${Number(value).toLocaleString(undefined, {minimumFractionDigits: decimals, maximumFractionDigits: decimals})}${suffix}`;
  }
  function fmtInt(value) {
    if (value === null || value === undefined || Number.isNaN(value)) return "";
    if (!finite(value)) return value > 0 ? "inf" : "-inf";
    return Math.round(value).toLocaleString();
  }
  function escapeHtml(value) {
    return String(value ?? "")
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#39;");
  }
  function renderTable(target, columns, rows) {
    const head = columns.map(column => `<th>${escapeHtml(column)}</th>`).join("");
    const body = rows.map(row => `<tr>${columns.map(column => `<td>${escapeHtml(row[column])}</td>`).join("")}</tr>`).join("");
    target.innerHTML = `<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
  }
  function cashCost(gasUnits, gasGwei, ethUsd, infraUsd) {
    return gasUnits * gasGwei * 1e-9 * ethUsd + infraUsd;
  }
  function maintenanceCoverage(params) {
    if (params.increment <= 0) return Infinity;
    return Math.min(1, params.decay_rate / params.increment);
  }
  function shareFromScore(score, params) {
    const capped = Math.min(Math.max(score, 0), params.max_score);
    const raw = params.k - params.a * Math.pow(params.max_score - capped, 2) / 1e10;
    return Math.max(raw, params.minimum);
  }
  function successfulUpdate(priorScore, elapsedEpochs, params) {
    const decayed = Math.max(0, priorScore - params.decay_rate * elapsedEpochs);
    return Math.min(params.max_score, decayed + params.increment);
  }
  function rewardFraction(proverShare, params, activeProvers) {
    const active = Math.max(Math.round(activeProvers), 1);
    const peers = Math.max(active - 1, 0);
    const denominator = peers * params.k + proverShare;
    return denominator <= 0 ? 0 : proverShare / denominator;
  }
  function recoveryRows(params, missedEpochs) {
    const missed = Math.max(Math.round(missedEpochs), 0);
    const rows = [];
    for (let i = 1; i <= missed; i += 1) {
      rows.push({event: "missed_epoch", timeline_epoch: i, proof_index_after_return: null, score: null, share: 0, relative_weight: 0, at_full_weight: false, raw_weight_loss_eq: 1});
    }
    const firstElapsed = missed === 0 ? 1 : missed + 1;
    let score = successfulUpdate(params.max_score, firstElapsed, params);
    for (let proofIdx = 1; proofIdx <= 50000; proofIdx += 1) {
      const share = shareFromScore(score, params);
      const atFull = score >= params.max_score - 1e-9;
      rows.push({
        event: "recovery_proof",
        timeline_epoch: missed + proofIdx,
        proof_index_after_return: proofIdx,
        score,
        share,
        relative_weight: share / params.k,
        at_full_weight: atFull,
        raw_weight_loss_eq: Math.max(0, 1 - share / params.k)
      });
      if (atFull) break;
      score = successfulUpdate(score, 1, params);
    }
    return rows;
  }
  function addEconomics(rows, params, econ) {
    const gasUsd = econ.gas_units_per_proof * econ.gas_price_gwei * 1e-9 * econ.eth_usd;
    const cashUsd = gasUsd + econ.infra_cost_per_proof_usd;
    const poolUsd = econ.epoch_reward_pool_aztc * econ.aztc_usd;
    const healthyRewardUsd = poolUsd / Math.max(Math.round(econ.active_full_weight_provers), 1);
    let cumulativeCash = 0;
    let cumulativeLoss = 0;
    let cumulativeRawLoss = 0;
    return rows.map(row => {
      let rewardUsd = 0;
      let cashSpend = 0;
      let profitUsd = 0;
      let lossUsd = healthyRewardUsd;
      if (row.event === "recovery_proof") {
        rewardUsd = poolUsd * rewardFraction(row.share, params, econ.active_full_weight_provers);
        cashSpend = cashUsd;
        profitUsd = rewardUsd - cashSpend;
        lossUsd = Math.max(0, healthyRewardUsd - rewardUsd);
      }
      cumulativeCash += cashSpend;
      cumulativeLoss += lossUsd;
      cumulativeRawLoss += row.raw_weight_loss_eq;
      return {...row, reward_usd: rewardUsd, cash_spend_usd: cashSpend, profit_usd: profitUsd, reward_opportunity_loss_usd: lossUsd, cumulative_cash_spend_usd: cumulativeCash, cumulative_reward_opportunity_loss_usd: cumulativeLoss, cumulative_raw_weight_loss_eq: cumulativeRawLoss};
    });
  }
  function breakEvenShare(params, cashUsd, econ) {
    const poolUsd = econ.epoch_reward_pool_aztc * econ.aztc_usd;
    if (poolUsd <= 0) return {share: Infinity, relative: Infinity, status: "No rewards: AZTC price or reward pool is zero."};
    const active = Math.max(Math.round(econ.active_full_weight_provers), 1);
    const peers = Math.max(active - 1, 0);
    const requiredFraction = cashUsd / poolUsd;
    const maxFraction = 1 / active;
    if (requiredFraction <= 0) return {share: 0, relative: 0, status: "Profitable at any positive reward share."};
    if (requiredFraction > maxFraction + 1e-15) return {share: Infinity, relative: Infinity, status: "Not profitable even at full weight."};
    if (peers === 0) return {share: 0, relative: 0, status: "Single-prover case: any positive share receives the whole pool."};
    const share = (requiredFraction * peers * params.k) / (1 - requiredFraction);
    return {share, relative: share / params.k, status: "Break-even share found."};
  }
  function scoreForShare(targetShare, params) {
    if (!finite(targetShare)) return Infinity;
    const target = Math.max(targetShare, params.minimum);
    if (target <= params.minimum) return 0;
    if (target >= params.k) return params.max_score;
    const delta = Math.sqrt(Math.max(0, (params.k - target) * 1e10 / params.a));
    return Math.max(0, params.max_score - delta);
  }
  function breakEvenAztc(cashUsd, econ) {
    if (econ.epoch_reward_pool_aztc <= 0) return Infinity;
    return cashUsd * Math.max(Math.round(econ.active_full_weight_provers), 1) / econ.epoch_reward_pool_aztc;
  }
  function breakEvenGas(econ) {
    const reward = econ.epoch_reward_pool_aztc * econ.aztc_usd / Math.max(Math.round(econ.active_full_weight_provers), 1);
    const remaining = reward - econ.infra_cost_per_proof_usd;
    if (remaining < 0 || econ.eth_usd <= 0 || econ.gas_units_per_proof <= 0) return -Infinity;
    return remaining / (econ.gas_units_per_proof * 1e-9 * econ.eth_usd);
  }
  function summarize(params, missed, econ) {
    const rows = addEconomics(recoveryRows(params, missed), params, econ);
    const proofs = rows.filter(row => row.event === "recovery_proof");
    const firstProof = proofs[0];
    const cashPerProof = cashCost(econ.gas_units_per_proof, econ.gas_price_gwei, econ.eth_usd, econ.infra_cost_per_proof_usd);
    const be = breakEvenShare(params, cashPerProof, econ);
    const recoveryProofs = proofs.length;
    return {
      parameter_set: params.name,
      first_return_share_pct: 100 * firstProof.share / params.k,
      underweight_recovery_proofs: proofs.filter(row => row.share < params.k - 1e-9).length,
      recovery_proofs_to_full_weight: recoveryProofs,
      recovery_time_hours: recoveryProofs * DEFAULTS.epoch_minutes / 60,
      raw_reward_weight_loss_eq: rows.reduce((sum, row) => sum + row.raw_weight_loss_eq, 0),
      reward_opportunity_loss_usd: rows.reduce((sum, row) => sum + row.reward_opportunity_loss_usd, 0),
      gross_recovery_cash_spend_usd: proofs.reduce((sum, row) => sum + row.cash_spend_usd, 0),
      cash_cost_per_proof_usd: cashPerProof,
      full_weight_reward_per_proof_usd: econ.epoch_reward_pool_aztc * econ.aztc_usd / Math.max(Math.round(econ.active_full_weight_provers), 1),
      full_weight_profit_per_proof_usd: econ.epoch_reward_pool_aztc * econ.aztc_usd / Math.max(Math.round(econ.active_full_weight_provers), 1) - cashPerProof,
      break_even_relative_weight_pct: finite(be.relative) ? 100 * be.relative : Infinity,
      break_even_score: scoreForShare(be.share, params),
      break_even_aztc_usd_at_full_weight: breakEvenAztc(cashPerProof, econ),
      break_even_gas_gwei_at_full_weight: breakEvenGas(econ),
      break_even_status: be.status
    };
  }
  function trace(name, x, y, xaxis = "x", yaxis = "y", color, mode = "lines+markers", dash = null) {
    const line = {width: 2};
    const marker = {size: 7};
    if (color) { line.color = color; marker.color = color; }
    if (dash) line.dash = dash;
    return {type: "scatter", mode, name, x, y, xaxis, yaxis, line, marker, showlegend: false};
  }
  function lineTrace(name, x, y, xaxis = "x", yaxis = "y", color = "#6b7280", dash = "dash") {
    return {type: "scatter", mode: "lines", name, x, y, xaxis, yaxis, line: {color, dash, width: 2}, showlegend: false};
  }
  function legendItem(name, color, dash = null) {
    return {name, color, dash};
  }
  function renderLegend(target, items) {
    target.innerHTML = items.map(item => `<span class="aztec-legend-item"><span class="aztec-legend-line ${item.dash || ""}" style="--legend-color: ${escapeHtml(item.color)}"></span><span>${escapeHtml(item.name)}</span></span>`).join("");
  }
  function basePanelLayout(xTitle, yTitle, extra = {}) {
    return {
      autosize: true,
      height: 300,
      margin: {l: 58, r: 18, t: 10, b: 50},
      showlegend: false,
      hovermode: "closest",
      xaxis: {title: {text: xTitle}, automargin: true},
      yaxis: {title: {text: yTitle}, automargin: true},
      ...extra
    };
  }
  function panelTraces(traces, xaxis, yaxis) {
    return traces.filter(trace => trace.xaxis === xaxis && trace.yaxis === yaxis).map(trace => ({...trace, xaxis: "x", yaxis: "y"}));
  }
  function legendFromTraces(traces) {
    return traces.map(trace => legendItem(trace.name, trace.line?.color || trace.marker?.color || "#6b7280", trace.line?.dash || null));
  }
  function chartPanel(plotId, legendId, traces, xaxis, yaxis, xTitle, yTitle, extra = {}) {
    const selected = panelTraces(traces, xaxis, yaxis);
    return {plotId, legendId, traces: selected, legend: legendFromTraces(selected), layout: basePanelLayout(xTitle, yTitle, extra)};
  }
  function renderPanels(panels, config) {
    panels.forEach(panel => {
      Plotly.react(byId(panel.plotId), panel.traces, panel.layout, config);
      renderLegend(byId(panel.legendId), panel.legend);
    });
  }
  function subplotTitle(text, xDomain, y) {
    return {
      text,
      x: (xDomain[0] + xDomain[1]) / 2,
      y,
      xref: "paper",
      yref: "paper",
      xanchor: "center",
      yanchor: "bottom",
      align: "center",
      showarrow: false,
      font: {size: 13}
    };
  }
  function setPlotStatus(status, message) {
    if (!status) return;
    status.textContent = message;
    status.hidden = message === "";
  }
  function waitForPlotly(status, render, attempt = 0) {
    if (window.Plotly) { setPlotStatus(status, ""); return true; }
    setPlotStatus(status, "Loading Plotly.js...");
    if (attempt >= 200) {
      setPlotStatus(status, "Plotly.js did not load. Check network access to cdn.plot.ly.");
      return false;
    }
    window.setTimeout(() => {
      if (window.Plotly) { setPlotStatus(status, ""); render(); }
      else waitForPlotly(status, render, attempt + 1);
    }, 50);
    return false;
  }
  function econFrom(prefix) {
    return {
      active_full_weight_provers: Math.max(1, Math.round(num(`${prefix}-active-provers`))),
      epoch_reward_pool_aztc: Math.max(0, num(`${prefix}-reward-pool`)),
      aztc_usd: Math.max(0, num(`${prefix}-aztc-usd`)),
      eth_usd: Math.max(0, num(`${prefix}-eth-usd`)),
      gas_price_gwei: Math.max(0, num(`${prefix}-gas-gwei`)),
      gas_units_per_proof: Math.max(0, num(`${prefix}-gas-units`)),
      infra_cost_per_proof_usd: Math.max(0, num(`${prefix}-infra-cost`))
    };
  }
  function setDefaultEconomics(prefix) {
    setVal(`${prefix}-active-provers`, DEFAULTS.active_full_weight_provers);
    setVal(`${prefix}-reward-pool`, DEFAULTS.epoch_reward_pool_aztc);
    setVal(`${prefix}-aztc-usd`, DEFAULTS.aztc_usd);
    setVal(`${prefix}-eth-usd`, DEFAULTS.eth_usd);
    setVal(`${prefix}-gas-gwei`, DEFAULTS.gas_price_gwei);
    setVal(`${prefix}-gas-units`, DEFAULTS.gas_units_per_proof);
    setVal(`${prefix}-infra-cost`, DEFAULTS.infra_cost_per_proof_usd);
  }
  function comparisonSummaryRows(missed, econ) {
    return Object.values(PRESETS).map(params => {
      const row = summarize(params, missed, econ);
      return {
        "Parameter set": row.parameter_set,
        "First return": fmtNumber(row.first_return_share_pct, 2, "", "%"),
        "Recovery proofs": fmtInt(row.recovery_proofs_to_full_weight),
        "Recovery time": fmtNumber(row.recovery_time_hours, 2, "", " h"),
        "Reward loss": fmtNumber(row.reward_opportunity_loss_usd, 2, "$"),
        "Gross cash": fmtNumber(row.gross_recovery_cash_spend_usd, 2, "$"),
        "Break-even": row.break_even_status
      };
    });
  }
  function buildComparisonState(missed, econ) {
    const traces = [];
    let recoveryMax = 1;
    for (const params of Object.values(PRESETS)) {
      const rows = addEconomics(recoveryRows(params, missed), params, econ);
      const proofs = rows.filter(row => row.event === "recovery_proof");
      recoveryMax = Math.max(recoveryMax, ...proofs.map(row => row.proof_index_after_return));
      traces.push(trace(params.name, proofs.map(row => row.proof_index_after_return), proofs.map(row => 100 * row.relative_weight), "x", "y", COLORS[params.name]));
      traces.push(trace(params.name, proofs.map(row => row.proof_index_after_return), proofs.map(row => row.cumulative_cash_spend_usd), "x2", "y2", COLORS[params.name], "lines+markers", null, true, "legend2"));
    }
    traces.push(lineTrace("full weight", [1, recoveryMax], [100, 100], "x", "y"));
    const missRange = intRange(1, Math.max(10, missed));
    for (const params of Object.values(PRESETS)) {
      const firstReturnShares = missRange.map(m => {
        const firstProof = recoveryRows(params, m).find(row => row.event === "recovery_proof");
        return 100 * firstProof.share / params.k;
      });
      traces.push(trace(params.name, missRange, firstReturnShares, "x3", "y3", COLORS[params.name], "lines+markers", null, true, "legend3"));
    }
    const gasGrid = linspace(0, Math.max(1, econ.gas_price_gwei * 3), 100);
    const fullReward = econ.epoch_reward_pool_aztc * econ.aztc_usd / Math.max(Math.round(econ.active_full_weight_provers), 1);
    const profits = gasGrid.map(g => fullReward - cashCost(econ.gas_units_per_proof, g, econ.eth_usd, econ.infra_cost_per_proof_usd));
    const profitMin = Math.min(...profits);
    const profitMax = Math.max(...profits);
    const profitPad = Math.max(1, 0.05 * (profitMax - profitMin));
    traces.push(trace("full-weight profit", gasGrid, profits, "x4", "y4", "#1f77b4", "lines", null, true, "legend4"));
    traces.push(lineTrace("break-even", [gasGrid[0], gasGrid[gasGrid.length - 1]], [0, 0], "x4", "y4", "#6b7280", "dash", true, "legend4"));
    traces.push(lineTrace("configured gas price", [econ.gas_price_gwei, econ.gas_price_gwei], [profitMin - profitPad, profitMax + profitPad], "x4", "y4", "#ef4444", "dot", true, "legend4"));
    return {
      title: `Recovery and cost summary after ${missed} missed epoch(s)`,
      panels: [
        chartPanel("aztec-comparison-recovery-plot", "aztec-comparison-recovery-legend", traces, "x", "y", "Successful proofs after return", "Relative reward weight (%)"),
        chartPanel("aztec-comparison-cash-plot", "aztec-comparison-cash-legend", traces, "x2", "y2", "Successful proofs after return", "Cumulative gross recovery cash spend (USD)"),
        chartPanel("aztec-comparison-penalty-plot", "aztec-comparison-penalty-legend", traces, "x3", "y3", "Consecutive missed epochs before returning", "Share on first return proof (%)"),
        chartPanel("aztec-comparison-profit-plot", "aztec-comparison-profit-legend", traces, "x4", "y4", "Gas price (gwei)", "Full-weight profit per proof (USD)", {yaxis: {title: {text: "Full-weight profit per proof (USD)"}, automargin: true, range: [profitMin - profitPad, profitMax + profitPad]}})
      ]
    };
  }
  function renderComparison() {
    setDefaultEconomics("aztec-comparison");
    const slider = byId("aztec-comparison-missed");
    const sliderValue = byId("aztec-comparison-missed-value");
    const table = byId("aztec-comparison-summary");
    const status = byId("aztec-comparison-plot-status");
    const title = byId("aztec-comparison-plot-title");
    const config = {responsive: true, displayModeBar: true};
    function render() {
      const missed = Math.round(num("aztec-comparison-missed"));
      const econ = econFrom("aztec-comparison");
      sliderValue.value = missed;
      renderTable(table, comparisonColumns, comparisonSummaryRows(missed, econ));
      if (!waitForPlotly(status, render)) return;
      const state = buildComparisonState(missed, econ);
      title.textContent = state.title;
      renderPanels(state.panels, config);
    }
    byId("aztec-comparison-widget").querySelectorAll("input").forEach(input => input.addEventListener("input", render));
    render();
  }
  function pathRowsForTable(rows) {
    return rows.slice(0, 50).map(row => ({
      event: row.event,
      timeline_epoch: fmtInt(row.timeline_epoch),
      proof_index_after_return: row.proof_index_after_return === null ? "" : fmtInt(row.proof_index_after_return),
      score: row.score === null ? "" : fmtInt(row.score),
      relative_weight: fmtNumber(100 * row.relative_weight, 2, "", "%"),
      reward_usd: fmtNumber(row.reward_usd, 2, "$"),
      cash_spend_usd: fmtNumber(row.cash_spend_usd, 2, "$"),
      profit_usd: fmtNumber(row.profit_usd, 2, "$"),
      reward_opportunity_loss_usd: fmtNumber(row.reward_opportunity_loss_usd, 2, "$"),
      cumulative_cash_spend_usd: fmtNumber(row.cumulative_cash_spend_usd, 2, "$"),
      cumulative_reward_opportunity_loss_usd: fmtNumber(row.cumulative_reward_opportunity_loss_usd, 2, "$"),
    }));
  }
  function singleSummaryRows(summary) {
    return [
      {Metric: "Recovery proofs to full weight", Value: fmtInt(summary.recovery_proofs_to_full_weight)},
      {Metric: "Underweight recovery proofs", Value: fmtInt(summary.underweight_recovery_proofs)},
      {Metric: "Recovery time", Value: fmtNumber(summary.recovery_time_hours, 2, "", " h")},
      {Metric: "First-return weight", Value: fmtNumber(summary.first_return_share_pct, 2, "", "%")},
      {Metric: "Gross recovery cash spend", Value: fmtNumber(summary.gross_recovery_cash_spend_usd, 2, "$" )},
      {Metric: "Reward opportunity loss", Value: fmtNumber(summary.reward_opportunity_loss_usd, 2, "$" )},
      {Metric: "Full-weight reward/proof", Value: fmtNumber(summary.full_weight_reward_per_proof_usd, 2, "$" )},
      {Metric: "Cash cost/proof", Value: fmtNumber(summary.cash_cost_per_proof_usd, 2, "$" )},
      {Metric: "Break-even status", Value: summary.break_even_status}
    ];
  }
  function customParams() {
    return {
      name: "Custom",
      increment: num("aztec-custom-increment"),
      max_score: num("aztec-custom-max-score"),
      a: num("aztec-custom-a"),
      minimum: num("aztec-custom-minimum"),
      k: num("aztec-custom-k"),
      decay_rate: num("aztec-custom-decay-rate")
    };
  }
  function paramsForSingle() {
    const selected = byId("aztec-single-preset").value;
    return selected === "Custom" ? customParams() : PRESETS[selected];
  }
  function buildSingleState(params, missed, econ) {
    const rows = addEconomics(recoveryRows(params, missed), params, econ);
    const proofs = rows.filter(row => row.event === "recovery_proof");
    const summary = summarize(params, missed, econ);
    const cashPerProof = summary.cash_cost_per_proof_usd;
    const be = breakEvenShare(params, cashPerProof, econ);
    const requiredScore = scoreForShare(be.share, params);
    const scoreGrid = linspace(0, params.max_score, 500);
    const missRange = intRange(1, 20);
    const firstReturnRows = missRange.map(m => {
      const firstRows = addEconomics(recoveryRows(params, m), params, econ);
      return firstRows.find(row => row.event === "recovery_proof");
    });
    const proofX = proofs.map(row => row.proof_index_after_return);
    const proofMin = Math.min(...proofX);
    const proofMax = Math.max(...proofX);
    const costX = rows.map(row => row.timeline_epoch);
    const rewardProfitValues = proofs.flatMap(row => [row.reward_usd, row.profit_usd]).concat([cashPerProof, 0]);
    const rewardProfitMin = Math.min(...rewardProfitValues);
    const rewardProfitMax = Math.max(...rewardProfitValues);
    const rewardProfitPad = Math.max(1, 0.05 * (rewardProfitMax - rewardProfitMin));
    const firstProfit = firstReturnRows.map(row => row.profit_usd);
    const firstProfitMin = Math.min(...firstProfit, 0);
    const firstProfitMax = Math.max(...firstProfit, 0);
    const firstProfitPad = Math.max(1, 0.05 * (firstProfitMax - firstProfitMin));
    const traces = [
      trace("share curve", scoreGrid.map(score => score / params.max_score), scoreGrid.map(score => 100 * shareFromScore(score, params) / params.k), "x", "y", "#1f77b4", "lines"),
      trace("recovery proofs", proofs.map(row => row.score / params.max_score), proofs.map(row => 100 * row.relative_weight), "x", "y", "#ff7f0e", "markers"),
      trace("relative weight", proofX, proofs.map(row => 100 * row.relative_weight), "x2", "y2", "#1f77b4", "lines+markers", null, true, "legend2"),
      lineTrace("full weight", [proofMin, proofMax], [100, 100], "x2", "y2", "#6b7280", "dash", true, "legend2"),
      trace("reward per proof", proofX, proofs.map(row => row.reward_usd), "x3", "y3", "#1f77b4", "lines+markers", null, true, "legend3"),
      lineTrace("cash cost per proof", [proofMin, proofMax], [cashPerProof, cashPerProof], "x3", "y3", "#ef4444", "dash", true, "legend3"),
      trace("profit per proof", proofX, proofs.map(row => row.profit_usd), "x3", "y3", "#2ca02c", "lines+markers", null, true, "legend3"),
      lineTrace("break-even profit", [proofMin, proofMax], [0, 0], "x3", "y3", "#6b7280", "dash", true, "legend3"),
      trace("cumulative gross cash spend", rows.map(row => row.timeline_epoch), rows.map(row => row.cumulative_cash_spend_usd), "x4", "y4", "#1f77b4", "lines+markers", null, true, "legend4"),
      trace("cumulative reward opportunity loss", rows.map(row => row.timeline_epoch), rows.map(row => row.cumulative_reward_opportunity_loss_usd), "x4", "y4", "#ff7f0e", "lines+markers", null, true, "legend4"),
      trace("first-return weight", missRange, firstReturnRows.map(row => 100 * row.relative_weight), "x5", "y5", "#1f77b4", "lines+markers", null, true, "legend5"),
      trace("first-return profit", missRange, firstProfit, "x6", "y6", "#1f77b4", "lines+markers", null, true, "legend6"),
      lineTrace("break-even profit", [1, 20], [0, 0], "x6", "y6", "#6b7280", "dash", true, "legend6")
    ];
    if (finite(be.relative)) {
      traces.push(lineTrace("break-even relative weight", [0, 1], [100 * be.relative, 100 * be.relative], "x", "y"));
      traces.push(lineTrace("break-even relative weight", [proofMin, proofMax], [100 * be.relative, 100 * be.relative], "x2", "y2", "#6b7280", "dot", true, "legend2"));
      traces.push(lineTrace("break-even relative weight", [1, 20], [100 * be.relative, 100 * be.relative], "x5", "y5", "#6b7280", "dash", true, "legend5"));
      if (finite(requiredScore)) traces.push(lineTrace("break-even score", [requiredScore / params.max_score, requiredScore / params.max_score], [0, 105], "x", "y", "#ef4444", "dot"));
    }
    return {
      summary,
      rows,
      headline: `After ${missed} missed epoch(s), ${params.name} needs ${fmtInt(proofs.length)} successful proof(s) to reach the first full-weight proof. Gross recovery cash spend: ${fmtNumber(summary.gross_recovery_cash_spend_usd, 2, "$")}. Reward opportunity loss: ${fmtNumber(summary.reward_opportunity_loss_usd, 2, "$")}. Full-weight reward/proof: ${fmtNumber(summary.full_weight_reward_per_proof_usd, 2, "$")}. Cash cost/proof: ${fmtNumber(summary.cash_cost_per_proof_usd, 2, "$")}.`,
      title: `${params.name}: recovery panels after ${missed} missed epoch(s)`,
      panels: [
        chartPanel("aztec-single-reward-plot", "aztec-single-reward-legend", traces, "x", "y", "Score / maxScore", "Relative weight (%)", {yaxis: {title: {text: "Relative weight (%)"}, automargin: true, range: [0, 105]}}),
        chartPanel("aztec-single-recovery-plot", "aztec-single-recovery-legend", traces, "x2", "y2", "Successful proofs after return", "Relative weight (%)"),
        chartPanel("aztec-single-revenue-plot", "aztec-single-revenue-legend", traces, "x3", "y3", "Successful proofs after return", "USD", {yaxis: {title: {text: "USD"}, automargin: true, range: [rewardProfitMin - rewardProfitPad, rewardProfitMax + rewardProfitPad]}}),
        chartPanel("aztec-single-cost-plot", "aztec-single-cost-legend", traces, "x4", "y4", "Timeline epoch after last healthy proof", "USD", {xaxis: {title: {text: "Timeline epoch after last healthy proof"}, automargin: true, range: [Math.min(...costX), Math.max(...costX)]}}),
        chartPanel("aztec-single-first-weight-plot", "aztec-single-first-weight-legend", traces, "x5", "y5", "Consecutive missed epochs", "First-return relative weight (%)"),
        chartPanel("aztec-single-first-profit-plot", "aztec-single-first-profit-legend", traces, "x6", "y6", "Consecutive missed epochs", "First-return proof profit (USD)", {yaxis: {title: {text: "First-return proof profit (USD)"}, automargin: true, range: [firstProfitMin - firstProfitPad, firstProfitMax + firstProfitPad]}})
      ]
    };
  }
  function setDefaultCustomParams() {
    const params = PRESETS["AZIP-5 proposed"];
    setVal("aztec-custom-increment", params.increment);
    setVal("aztec-custom-max-score", params.max_score);
    setVal("aztec-custom-a", params.a);
    setVal("aztec-custom-minimum", params.minimum);
    setVal("aztec-custom-k", params.k);
    setVal("aztec-custom-decay-rate", params.decay_rate);
  }
  function renderSingle() {
    setDefaultEconomics("aztec-single");
    setDefaultCustomParams();
    const presetSelect = byId("aztec-single-preset");
    presetSelect.innerHTML = Object.keys(PRESETS).concat(["Custom"]).map(name => `<option value="${name}">${escapeHtml(name)}</option>`).join("");
    presetSelect.value = "AZIP-5 proposed";
    const config = {responsive: true, displayModeBar: true};
    function render() {
      const missed = Math.round(num("aztec-single-missed"));
      const econ = econFrom("aztec-single");
      const params = paramsForSingle();
      byId("aztec-single-missed-value").value = missed;
      byId("aztec-single-custom").disabled = presetSelect.value !== "Custom";
      const state = buildSingleState(params, missed, econ);
      byId("aztec-single-headline").textContent = state.headline;
      renderTable(byId("aztec-single-summary"), summaryColumns, singleSummaryRows(state.summary));
      renderTable(byId("aztec-single-path"), pathColumns, pathRowsForTable(state.rows));
      byId("aztec-single-path-note").textContent = state.rows.length > 50 ? `Showing first 50 of ${fmtInt(state.rows.length)} rows.` : "";
      const status = byId("aztec-single-plot-status");
      if (!waitForPlotly(status, render)) return;
      byId("aztec-single-plot-title").textContent = state.title;
      renderPanels(state.panels, config);
    }
    byId("aztec-single-widget").querySelectorAll("input, select").forEach(input => input.addEventListener("input", render));
    presetSelect.addEventListener("change", render);
    render();
  }
  window.aztecRewardSim = {renderComparison, renderSingle};
  renderComparison();
})();
</script>
"""
    html = html.replace("__PLOTLY_CDN__", PLOTLY_CDN_URL)
    html = html.replace("__PRESETS__", _script_safe_json(PLOTLY_PRESETS))
    html = html.replace("__DEFAULTS__", _script_safe_json(PLOTLY_DEFAULTS))
    display(HTML(html))


render_preset_comparison_plotly_controls()

Side-by-side recovery and cost summary

1

Recovery path

Gross cash spend
to reach full weight

Penalty cliff

Full-weight profitability

Interactive single-parameter-set simulator

Use this section to inspect each preset with browser-side Plotly controls. The controls update precomputed recovery states on GitHub Pages without a live Python kernel.

Code

def render_single_scenario_plotly_controls():
    html = """
<div class="aztec-plotly-widget" id="aztec-single-widget">
  <div class="aztec-control-grid">
    <label>Preset <select id="aztec-single-preset"></select></label>
    <label>Missed epochs <input id="aztec-single-missed" type="range" min="1" max="20" step="1" value="1" /></label>
    <output id="aztec-single-missed-value">1</output>
    <label>Active provers <input id="aztec-single-active-provers" type="number" min="1" step="1" /></label>
    <label>Epoch reward AZTC <input id="aztec-single-reward-pool" type="number" min="0" step="100" /></label>
    <label>AZTC/USD <input id="aztec-single-aztc-usd" type="number" min="0" step="0.001" /></label>
    <label>ETH/USD <input id="aztec-single-eth-usd" type="number" min="0" step="100" /></label>
    <label>Gas gwei <input id="aztec-single-gas-gwei" type="number" min="0" step="0.1" /></label>
    <label>Gas units/proof <input id="aztec-single-gas-units" type="number" min="0" step="1000" /></label>
    <label>Infra $/proof <input id="aztec-single-infra-cost" type="number" min="0" step="1" /></label>
  </div>
  <fieldset id="aztec-single-custom" class="aztec-custom-fieldset">
    <legend>Custom parameters used only when Preset = Custom</legend>
    <div class="aztec-control-grid">
      <label>increment <input id="aztec-custom-increment" type="number" min="0" step="100" /></label>
      <label>maxScore <input id="aztec-custom-max-score" type="number" min="1" step="100" /></label>
      <label>a <input id="aztec-custom-a" type="number" min="0" step="100" /></label>
      <label>minimum <input id="aztec-custom-minimum" type="number" min="0" step="100" /></label>
      <label>k <input id="aztec-custom-k" type="number" min="1" step="1000" /></label>
      <label>decayRate <input id="aztec-custom-decay-rate" type="number" min="0" step="100" /></label>
    </div>
  </fieldset>
  <div id="aztec-single-headline" class="aztec-headline"></div>
  <div class="aztec-table-grid">
    <div>
      <h4>Scenario summary</h4>
      <div id="aztec-single-summary" class="aztec-summary-table"></div>
    </div>
    <div>
      <h4>Recovery path table</h4>
      <div id="aztec-single-path" class="aztec-summary-table"></div>
      <div id="aztec-single-path-note" class="aztec-path-note"></div>
    </div>
  </div>
  <div id="aztec-single-plot-status" class="aztec-plot-status"></div>
  <div id="aztec-single-plot-title" class="aztec-plot-title"></div>
  <div id="aztec-single-plot" class="aztec-panel-grid aztec-single-grid">
    <section class="aztec-panel"><h4>Reward curve<br>and profitability threshold</h4><div id="aztec-single-reward-plot" class="aztec-panel-plot"></div><div id="aztec-single-reward-legend" class="aztec-panel-legend"></div></section>
    <section class="aztec-panel"><h4>Recovery to full<br>reward weight</h4><div id="aztec-single-recovery-plot" class="aztec-panel-plot"></div><div id="aztec-single-recovery-legend" class="aztec-panel-legend"></div></section>
    <section class="aztec-panel"><h4>Revenue, cost, and profit<br>during recovery</h4><div id="aztec-single-revenue-plot" class="aztec-panel-plot"></div><div id="aztec-single-revenue-legend" class="aztec-panel-legend"></div></section>
    <section class="aztec-panel"><h4>Cumulative cost to return<br>to full weight</h4><div id="aztec-single-cost-plot" class="aztec-panel-plot"></div><div id="aztec-single-cost-legend" class="aztec-panel-legend"></div></section>
    <section class="aztec-panel"><h4>First-return weight<br>after missed epochs</h4><div id="aztec-single-first-weight-plot" class="aztec-panel-plot"></div><div id="aztec-single-first-weight-legend" class="aztec-panel-legend"></div></section>
    <section class="aztec-panel"><h4>First-return proof<br>profitability</h4><div id="aztec-single-first-profit-plot" class="aztec-panel-plot"></div><div id="aztec-single-first-profit-legend" class="aztec-panel-legend"></div></section>
  </div>
</div>
<style>
  .aztec-custom-fieldset { border: 1px solid #d1d5db; margin: 8px 0 14px; padding: 10px 12px; }
  .aztec-custom-fieldset legend { font-size: 13px; font-weight: 600; padding: 0 6px; }
  .aztec-custom-fieldset:disabled { opacity: 0.55; }
  .aztec-headline { font-weight: 600; margin: 8px 0 12px; }
  .aztec-table-grid { display: grid; gap: 18px; grid-template-columns: 1fr; margin-bottom: 14px; }
  .aztec-table-grid h4 { margin: 0 0 6px; }
  .aztec-path-note { color: #4b5563; font-size: 12px; margin-top: 6px; }
  .aztec-single-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  @media (max-width: 760px) { .aztec-single-grid { grid-template-columns: 1fr; } }
</style>
<script>
(() => {
  if (!window.aztecRewardSim) {
    document.getElementById("aztec-single-widget").textContent = "Simulator controls did not initialize.";
    return;
  }
  window.aztecRewardSim.renderSingle();
})();
</script>
"""
    display(HTML(html))


render_single_scenario_plotly_controls()
1
Custom parameters used only when Preset = Custom

Scenario summary

Recovery path table

Reward curve
and profitability threshold

Recovery to full
reward weight

Revenue, cost, and profit
during recovery

Cumulative cost to return
to full weight

First-return weight
after missed epochs

First-return proof
profitability

Gas and infrastructure sensitivity tables

This section creates small sensitivity tables you can use for decision-making. It uses the selected/default gas units and infrastructure assumptions but sweeps gas price and ETH/USD.

Code

def gas_sensitivity_table(
    gas_units_per_proof: float = DEFAULT_GAS_UNITS_PER_PROOF,
    infra_cost_per_proof_usd: float = DEFAULT_INFRA_COST_PER_PROOF_USD,
    gas_prices_gwei=(1, 5, 10, 20, 50),
    eth_prices_usd=(2_000, 3_000, 4_000, 5_000),
) -> pd.DataFrame:
    rows = []
    for gas_gwei in gas_prices_gwei:
        for eth_usd in eth_prices_usd:
            gas_usd = gas_cost_usd(gas_units_per_proof, gas_gwei, eth_usd)
            rows.append({
                "gas_price_gwei": gas_gwei,
                "ETH_USD": eth_usd,
                "gas_cost_per_proof_usd": gas_usd,
                "infra_cost_per_proof_usd": infra_cost_per_proof_usd,
                "cash_cost_per_proof_usd": gas_usd + infra_cost_per_proof_usd,
            })
    return pd.DataFrame(rows)


def recovery_cash_sensitivity_table(
    missed_epochs: int = 1,
    gas_units_per_proof: float = DEFAULT_GAS_UNITS_PER_PROOF,
    infra_cost_per_proof_usd: float = DEFAULT_INFRA_COST_PER_PROOF_USD,
    gas_prices_gwei=(1, 5, 10, 20, 50),
    eth_prices_usd=(2_000, 3_000, 4_000, 5_000),
) -> pd.DataFrame:
    rows = []
    for preset_name, params in PARAMETER_PRESETS.items():
        proofs_to_full = len(recovery_dataframe(params, missed_epochs=missed_epochs).query("event == 'recovery_proof'"))
        for gas_gwei in gas_prices_gwei:
            for eth_usd in eth_prices_usd:
                cash = cash_cost_per_proof_usd(gas_units_per_proof, gas_gwei, eth_usd, infra_cost_per_proof_usd)
                rows.append({
                    "parameter_set": preset_name,
                    "missed_epochs": missed_epochs,
                    "recovery_proofs_to_full_weight": proofs_to_full,
                    "gas_price_gwei": gas_gwei,
                    "ETH_USD": eth_usd,
                    "cash_cost_per_proof_usd": cash,
                    "gross_recovery_cash_spend_usd": proofs_to_full * cash,
                })
    return pd.DataFrame(rows)

print("Gas + infra cost per proof:")
display(gas_sensitivity_table())

print("Gross recovery cash spend after one missed epoch:")
display(recovery_cash_sensitivity_table(missed_epochs=1))
Gas + infra cost per proof:
gas_price_gwei ETH_USD gas_cost_per_proof_usd infra_cost_per_proof_usd cash_cost_per_proof_usd
0 1 2000 7.700000 44.000000 51.700000
1 1 3000 11.550000 44.000000 55.550000
2 1 4000 15.400000 44.000000 59.400000
3 1 5000 19.250000 44.000000 63.250000
4 5 2000 38.500000 44.000000 82.500000
5 5 3000 57.750000 44.000000 101.750000
6 5 4000 77.000000 44.000000 121.000000
7 5 5000 96.250000 44.000000 140.250000
8 10 2000 77.000000 44.000000 121.000000
9 10 3000 115.500000 44.000000 159.500000
10 10 4000 154.000000 44.000000 198.000000
11 10 5000 192.500000 44.000000 236.500000
12 20 2000 154.000000 44.000000 198.000000
13 20 3000 231.000000 44.000000 275.000000
14 20 4000 308.000000 44.000000 352.000000
15 20 5000 385.000000 44.000000 429.000000
16 50 2000 385.000000 44.000000 429.000000
17 50 3000 577.500000 44.000000 621.500000
18 50 4000 770.000000 44.000000 814.000000
19 50 5000 962.500000 44.000000 1,006.500000
Gross recovery cash spend after one missed epoch:
parameter_set missed_epochs recovery_proofs_to_full_weight gas_price_gwei ETH_USD cash_cost_per_proof_usd gross_recovery_cash_spend_usd
0 Current / original 1 4 1 2000 51.700000 206.800000
1 Current / original 1 4 1 3000 55.550000 222.200000
2 Current / original 1 4 1 4000 59.400000 237.600000
3 Current / original 1 4 1 5000 63.250000 253.000000
4 Current / original 1 4 5 2000 82.500000 330.000000
5 Current / original 1 4 5 3000 101.750000 407.000000
6 Current / original 1 4 5 4000 121.000000 484.000000
7 Current / original 1 4 5 5000 140.250000 561.000000
8 Current / original 1 4 10 2000 121.000000 484.000000
9 Current / original 1 4 10 3000 159.500000 638.000000
10 Current / original 1 4 10 4000 198.000000 792.000000
11 Current / original 1 4 10 5000 236.500000 946.000000
12 Current / original 1 4 20 2000 198.000000 792.000000
13 Current / original 1 4 20 3000 275.000000 1,100.000000
14 Current / original 1 4 20 4000 352.000000 1,408.000000
15 Current / original 1 4 20 5000 429.000000 1,716.000000
16 Current / original 1 4 50 2000 429.000000 1,716.000000
17 Current / original 1 4 50 3000 621.500000 2,486.000000
18 Current / original 1 4 50 4000 814.000000 3,256.000000
19 Current / original 1 4 50 5000 1,006.500000 4,026.000000
20 AZIP-5 proposed 1 72 1 2000 51.700000 3,722.400000
21 AZIP-5 proposed 1 72 1 3000 55.550000 3,999.600000
22 AZIP-5 proposed 1 72 1 4000 59.400000 4,276.800000
23 AZIP-5 proposed 1 72 1 5000 63.250000 4,554.000000
24 AZIP-5 proposed 1 72 5 2000 82.500000 5,940.000000
25 AZIP-5 proposed 1 72 5 3000 101.750000 7,326.000000
26 AZIP-5 proposed 1 72 5 4000 121.000000 8,712.000000
27 AZIP-5 proposed 1 72 5 5000 140.250000 10,098.000000
28 AZIP-5 proposed 1 72 10 2000 121.000000 8,712.000000
29 AZIP-5 proposed 1 72 10 3000 159.500000 11,484.000000
30 AZIP-5 proposed 1 72 10 4000 198.000000 14,256.000000
31 AZIP-5 proposed 1 72 10 5000 236.500000 17,028.000000
32 AZIP-5 proposed 1 72 20 2000 198.000000 14,256.000000
33 AZIP-5 proposed 1 72 20 3000 275.000000 19,800.000000
34 AZIP-5 proposed 1 72 20 4000 352.000000 25,344.000000
35 AZIP-5 proposed 1 72 20 5000 429.000000 30,888.000000
36 AZIP-5 proposed 1 72 50 2000 429.000000 30,888.000000
37 AZIP-5 proposed 1 72 50 3000 621.500000 44,748.000000
38 AZIP-5 proposed 1 72 50 4000 814.000000 58,608.000000
39 AZIP-5 proposed 1 72 50 5000 1,006.500000 72,468.000000
40 htimsk softer revision 1 10 1 2000 51.700000 517.000000
41 htimsk softer revision 1 10 1 3000 55.550000 555.500000
42 htimsk softer revision 1 10 1 4000 59.400000 594.000000
43 htimsk softer revision 1 10 1 5000 63.250000 632.500000
44 htimsk softer revision 1 10 5 2000 82.500000 825.000000
45 htimsk softer revision 1 10 5 3000 101.750000 1,017.500000
46 htimsk softer revision 1 10 5 4000 121.000000 1,210.000000
47 htimsk softer revision 1 10 5 5000 140.250000 1,402.500000
48 htimsk softer revision 1 10 10 2000 121.000000 1,210.000000
49 htimsk softer revision 1 10 10 3000 159.500000 1,595.000000
50 htimsk softer revision 1 10 10 4000 198.000000 1,980.000000
51 htimsk softer revision 1 10 10 5000 236.500000 2,365.000000
52 htimsk softer revision 1 10 20 2000 198.000000 1,980.000000
53 htimsk softer revision 1 10 20 3000 275.000000 2,750.000000
54 htimsk softer revision 1 10 20 4000 352.000000 3,520.000000
55 htimsk softer revision 1 10 20 5000 429.000000 4,290.000000
56 htimsk softer revision 1 10 50 2000 429.000000 4,290.000000
57 htimsk softer revision 1 10 50 3000 621.500000 6,215.000000
58 htimsk softer revision 1 10 50 4000 814.000000 8,140.000000
59 htimsk softer revision 1 10 50 5000 1,006.500000 10,065.000000