import os import math import gradio as gr import pandas as pd from pathlib import Path from dotenv import load_dotenv load_dotenv() def calculate_gridcoin( owner, pv_kw, batt_kwh, annual_load, retail_rate, upfront, exp_actual, exp_evc, grid_fee, peak_start, peak_end, coin_value, peak_fraction, dr_kwh ): """Calculate GridCoin metrics and generate results.""" # Core calculations pv_yield = pv_kw * 1250 self_use_ratio = 0.65 self_used = pv_yield * self_use_ratio exports = max(0, pv_yield - self_used - batt_kwh * 200) peak_exports = exports * peak_fraction coins_peak = peak_exports * 1.0 coins_dr = dr_kwh * 0.5 coins_total = coins_peak + coins_dr coin_dollars = coins_total * coin_value bill_nonder = annual_load * retail_rate imports_der = max(0, annual_load - self_used) exp_credit = exports * exp_actual fee_der = grid_fee * pv_kw * 12 bill_der = imports_der * retail_rate + fee_der - exp_credit - coin_dollars savings = max(0, bill_nonder - bill_der) payback = upfront / savings if savings > 0 else math.inf # Format results gridcoins_earned = f"{coins_total:,.1f}" gridcoin_value = f"${coin_dollars:,.2f}" non_der_bill = f"${bill_nonder:,.0f}" der_bill = f"${bill_der:,.0f}" bill_savings = f"${bill_der - bill_nonder:,.0f}" payback_period = f"{payback:.1f} yrs" if payback != math.inf else "N/A" # AI Analysis ai_summary = "" if os.getenv("OPENAI_API_KEY"): try: import openai openai.api_key = os.getenv("OPENAI_API_KEY") prompt = ( f"Write a clear, well-formatted summary in 2-3 complete sentences with proper spacing:\n\n" f"A {owner} with a {pv_kw} kW solar system and {batt_kwh} kWh battery " f"earns {coins_total:.1f} GridCoins valued at ${coin_dollars:.2f} annually. " f"Their annual electricity bill is ${bill_der:,.0f} (DER) compared to ${bill_nonder:,.0f} (Non-DER), " f"with a payback period of {payback:.1f} years.\n\n" "Explain the financial benefits clearly and concisely. Use proper sentence structure with spaces between words." ) resp = openai.ChatCompletion.create( model="gpt-4o", messages=[ {"role": "system", "content": "You are a helpful energy policy assistant. Provide clear, concise summaries."}, {"role": "user", "content": prompt} ], max_tokens=150, temperature=0.7 ) ai_summary = resp.choices[0].message.content.strip() except Exception as e: ai_summary = f"AI summary unavailable: {e}" else: ai_summary = "Set OPENAI_API_KEY to enable AI summaries." return ( gridcoins_earned, gridcoin_value, non_der_bill, der_bill, bill_savings, payback_period, ai_summary ) # Custom CSS for better styling custom_css = """ .gradio-container { font-family: 'Inter', sans-serif; } .output-text { font-size: 1.2rem; font-weight: 600; } """ # Create Gradio interface with gr.Blocks(css=custom_css, title="GridCoin Calculator", theme=gr.themes.Soft()) as demo: gr.Markdown("# ⚡ GridCoin — DER vs Non-DER Dashboard") with gr.Row(): # Left column - Inputs with gr.Column(scale=1): gr.Markdown("### ⚙️ System Configuration") with gr.Row(): with gr.Column(): owner = gr.Dropdown( choices=["DER Homeowner", "Non-DER Homeowner"], value="DER Homeowner", label="Household type" ) pv_kw = gr.Number( value=7.0, minimum=0.0, maximum=50.0, step=0.5, label="Solar size (kW)" ) batt_kwh = gr.Number( value=0.0, minimum=0.0, maximum=40.0, step=0.5, label="Battery (kWh)" ) with gr.Column(): annual_load = gr.Number( value=12000, minimum=500, maximum=30000, label="Annual use (kWh)" ) retail_rate = gr.Number( value=0.13, minimum=0.01, maximum=1.0, step=0.001, label="Retail rate ($/kWh)" ) upfront = gr.Number( value=25000, minimum=0, maximum=100000, label="System cost ($)" ) gr.Markdown("---") gr.Markdown("### 💰 Policy & GridCoin Settings") with gr.Row(): with gr.Column(): exp_actual = gr.Number( value=0.075, minimum=0.0, maximum=1.0, step=0.001, label="Export credit ($/kWh)" ) exp_evc = gr.Number( value=0.123, minimum=0.0, maximum=1.0, step=0.001, label="EVC Bonus ($/kWh)" ) grid_fee = gr.Number( value=4.0, minimum=0.0, maximum=20.0, label="Grid fee ($/kW-mo)" ) with gr.Column(): peak_start = gr.Number( value=17, minimum=0, maximum=23, label="Peak start (hr)" ) peak_end = gr.Number( value=21, minimum=0, maximum=23, label="Peak end (hr)" ) coin_value = gr.Number( value=0.05, minimum=0.0, maximum=1.0, step=0.001, label="GridCoin value ($)" ) gr.Markdown("---") gr.Markdown("### 🔋 GridCoin Calculator") with gr.Row(): with gr.Column(): peak_fraction = gr.Slider( minimum=0.0, maximum=1.0, value=0.30, step=0.05, label="Peak export share" ) with gr.Column(): dr_kwh = gr.Number( value=0.0, minimum=0.0, maximum=5000.0, label="DR savings (kWh)" ) calculate_btn = gr.Button("Calculate Results", variant="primary", size="lg") # Right column - Outputs with gr.Column(scale=1): gr.Markdown("### 📊 Results Dashboard") with gr.Row(): gridcoins_earned = gr.Textbox(label="GridCoins Earned", interactive=False) gridcoin_value = gr.Textbox(label="GridCoin Value", interactive=False) gr.Markdown("---") gr.Markdown("### 💵 Annual Bill Comparison") with gr.Row(): non_der_bill = gr.Textbox(label="Non-DER Bill", interactive=False) der_bill = gr.Textbox(label="DER Bill", interactive=False) bill_savings = gr.Textbox(label="Savings", interactive=False) payback_period = gr.Textbox(label="Payback Period", interactive=False) gr.Markdown("---") gr.Markdown("### 🤖 AI Analysis") ai_summary = gr.Textbox( label="", lines=5, interactive=False, show_label=False ) # Connect the calculate button to the function calculate_btn.click( fn=calculate_gridcoin, inputs=[ owner, pv_kw, batt_kwh, annual_load, retail_rate, upfront, exp_actual, exp_evc, grid_fee, peak_start, peak_end, coin_value, peak_fraction, dr_kwh ], outputs=[ gridcoins_earned, gridcoin_value, non_der_bill, der_bill, bill_savings, payback_period, ai_summary ] ) # Auto-calculate on load demo.load( fn=calculate_gridcoin, inputs=[ owner, pv_kw, batt_kwh, annual_load, retail_rate, upfront, exp_actual, exp_evc, grid_fee, peak_start, peak_end, coin_value, peak_fraction, dr_kwh ], outputs=[ gridcoins_earned, gridcoin_value, non_der_bill, der_bill, bill_savings, payback_period, ai_summary ] ) if __name__ == "__main__": demo.launch(share=False, server_name="127.0.0.1", server_port=7860)