# Constants import numpy as np import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import streamlit as st from typing import Dict from utils.llm import summary_generation ONE_BR_UNITS = 23 TWO_BR_UNITS = 45 SOLAR_PANEL_RATING = 625 # W BATTERY_CAPACITY = 200 # Ah BATTERY_VOLTAGE = 96 # V SYSTEM_LOSSES = 0.20 FEED_IN_TARIFF = 12 # Lighting specifications LIGHTS_1BR = 5 LIGHTS_2BR = 12 LIGHT_POWER = 6 # Watts per light def initialize_session_state(): """Initialize session state variables""" defaults = { "solar_panels": 20, "batteries": 15, "panel_price": 13000, "battery_price": 39000, "grid_price": 28.44, } for key, value in defaults.items(): if key not in st.session_state: st.session_state[key] = value def calculate_lighting_consumption(occupancy_1br: float, occupancy_2br: float) -> float: """Calculate daily lighting consumption""" return ( (occupancy_1br * ONE_BR_UNITS * LIGHTS_1BR * LIGHT_POWER / 1000) + (occupancy_2br * TWO_BR_UNITS * LIGHTS_2BR * LIGHT_POWER / 1000) ) * 6 # 6 hours per day # assuming that 1br average usage is 250wh and for a 2br is 400wh def calculate_appliance_consumption( occupancy_1br: float, occupancy_2br: float ) -> float: """Calculate daily appliance consumption by subtracting the lighting usage from the average total consumption for each house type""" return ( occupancy_1br * ONE_BR_UNITS * (3 - (LIGHTS_1BR * LIGHT_POWER * 6 / 1000)) # + (500 * 24) # Fridge ) + ( occupancy_2br * TWO_BR_UNITS * (4 - (LIGHTS_2BR * LIGHT_POWER * 6 / 1000)) # + (500 * 24) # Fridge ) # Daily kWh def total_consumption( occupancy_1br: float, occupancy_2br: float, common_area: float ) -> float: """Calculate total monthly consumption""" lighting = calculate_lighting_consumption(occupancy_1br, occupancy_2br) appliances = calculate_appliance_consumption(occupancy_1br, occupancy_2br) return (lighting + appliances + common_area) * 30 # Monthly kWh def solar_production(panels: int) -> float: """Monthly solar production with losses""" daily_production = ( panels * SOLAR_PANEL_RATING * 6.5 * (1 - SYSTEM_LOSSES) / 1000 ) # 6.5 sun hours return daily_production * 30 # Monthly kWh def battery_storage(batteries: int) -> float: """Usable battery capacity""" return batteries * BATTERY_CAPACITY * BATTERY_VOLTAGE * 0.8 / 1000 # kWh def financial_analysis( consumption: float, common_area_consumption: float, production: float, storage: float, ) -> Dict: """Detailed financial calculations""" solar_used = min(production, consumption) surplus = max(0, production - consumption) feed_in_revenue = surplus * FEED_IN_TARIFF / 100 # Convert to Ksh from cents/kWh # Account for battery storage grid_purchased = max(0, consumption - common_area_consumption - solar_used) if storage > 0: # Battery can offset some grid purchases grid_offset = min(grid_purchased, storage) grid_purchased -= grid_offset # Money paid to owner if client used this instead o monthly_savings = ( consumption - (common_area_consumption * 30) * st.session_state.grid_price / 100 ) total_investment = ( st.session_state.solar_panels * st.session_state.panel_price + st.session_state.batteries * st.session_state.battery_price ) # Avoid division by zero if monthly_savings > 0: payback_years = total_investment / (monthly_savings * 12) else: payback_years = float("inf") return { "consumption": consumption, "production": production, "solar_contribution": min(100, (solar_used / max(1, consumption)) * 100), "grid_dependency": (grid_purchased / max(1, consumption)) * 100, "monthly_savings": monthly_savings, "payback_period": payback_years, "grid_purchased": grid_purchased, } def create_consumption_breakdown( occupancy_1br: float, occupancy_2br: float, common_area: float ): """Create detailed consumption breakdown""" breakdown = { "Lighting": calculate_lighting_consumption(occupancy_1br, occupancy_2br) * 30, "Appliances": calculate_appliance_consumption(occupancy_1br, occupancy_2br) * 30, "Common Areas": common_area * 30, } return pd.DataFrame.from_dict(breakdown, orient="index", columns=["kWh"]) # Streamlit Interface def main(): st.set_page_config(page_title="Solar Analysis Suite", page_icon="🌞", layout="wide") initialize_session_state() # Custom CSS st.markdown( """ """, unsafe_allow_html=True, ) # Header with logo col1, col2 = st.columns([1, 4]) with col1: st.image("https://img.icons8.com/fluency/96/000000/sun.png", width=100) with col2: st.title("🌞 Advanced Solar Performance Analyzer") st.markdown( "Optimize your apartment complex solar installation with data-driven insights" ) # Sidebar for system configuration with st.sidebar: st.header("System Configuration") # Add a nice header image st.image("https://img.icons8.com/color/96/000000/solar-panel.png", width=80) # Create tabs for different settings tab1, tab2 = st.tabs(["Hardware", "Pricing"]) with tab1: st.number_input( "Number of Solar Panels", 1, 300, step=5, key="solar_panels", help="Each panel rated at 625W", ) st.number_input( "Number of Batteries", 0, 150, step=5, key="batteries", help="Each battery has 200Ah capacity at 12V", ) with tab2: st.number_input( "Panel Price (Ksh)", 1000, 50000, step=500, key="panel_price", help="Cost per solar panel", ) st.number_input( "Battery Price (Ksh)", 5000, 100000, step=1000, key="battery_price", help="Cost per battery unit", ) st.number_input( "Grid Price (Ksh/kWh)", 10.0, 50.0, step=0.1, key="grid_price", help="Current electricity price from the grid", ) st.markdown("---") st.markdown( """ 📊 **System Totals** - **Total Panel Capacity**: {0:.1f} kW - **Total Battery Storage**: {1:.1f} kWh - **Total Investment**: ksh. {2:,.0f} """.format( st.session_state.solar_panels * SOLAR_PANEL_RATING / 1000, battery_storage(st.session_state.batteries), st.session_state.solar_panels * st.session_state.panel_price + st.session_state.batteries * st.session_state.battery_price, ) ) # Main content # Create scenarios with varying occupancy levels scenarios = {} # Common area consumption remains constant common_area_consumption = 23.544 # kWh per day # Generate scenarios with different occupancy combinations occupancy_levels = [0.0, 0.25, 0.50, 0.75, 1.0] # Create scenarios for 1BR fixed, varying 2BR for br1_level in occupancy_levels: for br2_level in occupancy_levels: scenario_name = f"1BR: {int(br1_level*100)}%, 2BR: {int(br2_level*100)}%" scenarios[scenario_name] = { "1br": br1_level, "2br": br2_level, "common": common_area_consumption, } # Analysis tabs st.markdown("---") tab1, tab2, tab3 = st.tabs( ["📊 Energy Analysis", "💰 Financial Metrics", "🔍 Detailed Breakdown"] ) # Prepare analysis data for all scenarios analysis_data = [] for name, params in scenarios.items(): consumption = total_consumption(params["1br"], params["2br"], params["common"]) production = solar_production(st.session_state.solar_panels) storage = battery_storage(st.session_state.batteries) financials = financial_analysis( consumption, common_area_consumption, production, storage ) analysis_data.append({"Scenario": name, **financials}) df = pd.DataFrame(analysis_data) # Tab 1: Energy Analysis with tab1: st.header("Energy Flow Analysis") # Allow filtering by 1BR occupancy one_br_filter = st.selectbox( "Filter by 1BR Occupancy", ["All"] + [f"{int(level*100)}%" for level in occupancy_levels], help="Filter scenarios by 1BR occupancy level", ) # Filter the dataframe based on selection filtered_df = df if one_br_filter != "All": occupancy_value = int(one_br_filter.replace("%", "")) filtered_df = df[df["Scenario"].str.contains(f"1BR: {occupancy_value}%")] # Chart 1: Energy Balance st.subheader("Energy Balance by Scenario") energy_fig = plt.figure(figsize=(12, 7)) ax = energy_fig.add_subplot(111) # Create data for stacked bar chart chart_data = filtered_df.copy() chart_data["grid_energy"] = chart_data["grid_purchased"] chart_data["solar_energy"] = ( chart_data["consumption"] - chart_data["grid_purchased"] ) # Create normalized stacked bar chart chart_data = chart_data.set_index("Scenario") energy_proportions = ( chart_data[["solar_energy", "grid_energy"]].div( chart_data["consumption"], axis=0 ) * 100 ) energy_proportions = energy_proportions.reset_index() # Reshape for seaborn energy_melt = pd.melt( energy_proportions, id_vars=["Scenario"], value_vars=["solar_energy", "grid_energy"], var_name="Energy Source", value_name="Percentage", ) # Rename for better labels energy_melt["Energy Source"] = energy_melt["Energy Source"].replace( {"solar_energy": "Solar Generated", "grid_energy": "Grid Purchased"} ) # Plot with seaborn sns.set_theme(style="whitegrid") sns.barplot( data=energy_melt, x="Scenario", y="Percentage", hue="Energy Source", palette=["#4CAF50", "#F44336"], ax=ax, ) ax.set_ylabel("Energy Contribution (%)") ax.set_title("Energy Source Distribution by Occupancy Scenario") plt.xticks(rotation=45, ha="right") plt.tight_layout() st.pyplot(energy_fig) # Detailed metrics col1, col2, col3 = st.columns(3) with col1: st.metric( "Avg. Solar Contribution", f"{filtered_df['solar_contribution'].mean():.1f}%", ( f"{filtered_df['solar_contribution'].mean() - 50:.1f}%" if filtered_df["solar_contribution"].mean() > 50 else f"{filtered_df['solar_contribution'].mean() - 50:.1f}%" ), ) with col2: st.metric( "Avg. Grid Dependency", f"{filtered_df['grid_dependency'].mean():.1f}%", ( f"{50 - filtered_df['grid_dependency'].mean():.1f}%" if filtered_df["grid_dependency"].mean() < 50 else f"{50 - filtered_df['grid_dependency'].mean():.1f}%" ), ) with col3: st.metric( "Production/Consumption Ratio", f"{(filtered_df['production'].mean() / filtered_df['consumption'].mean() * 100):.1f}%", ) with st.expander("🔍 Energy Flow Interpretation"): st.markdown( """ **Understanding the Chart:** - **Solar Contribution**: Percentage of total energy needs met directly by solar production - **Grid Dependency**: Remaining energy required from the grid - The ideal scenario shows high solar contribution with minimal grid dependency **Key Factors Affecting Energy Balance:** 1. **Occupancy Levels**: Higher occupancy means higher consumption, which may exceed solar capacity 2. **Solar System Size**: More panels increase production and reduce grid dependency 3. **Battery Storage**: Helps utilize excess daytime production for nighttime use """ ) # Tab 2: Financial Metrics with tab2: st.header("Financial Performance Analysis") # Allow filtering by 2BR occupancy two_br_filter = st.selectbox( "Filter by 2BR Occupancy", ["All"] + [f"{int(level*100)}%" for level in occupancy_levels], help="Filter scenarios by 2BR occupancy level", ) # Filter the dataframe based on selection filtered_fin_df = df if two_br_filter != "All": occupancy_value = int(two_br_filter.replace("%", "")) filtered_fin_df = df[ df["Scenario"].str.contains(f"2BR: {occupancy_value}%") ] # Monthly Savings Chart st.subheader("Monthly Cost Savings") # Fix large values filtered_fin_df["monthly_savings_fixed"] = filtered_fin_df[ "monthly_savings" ].clip(0, 100000) fig1, ax1 = plt.subplots(figsize=(12, 6)) sns.barplot( data=filtered_fin_df, x="Scenario", y="monthly_savings_fixed", palette="viridis", ax=ax1, ) ax1.set_title("Monthly Cost Savings by Scenario") ax1.set_ylabel("Ksh") plt.xticks(rotation=45, ha="right") plt.tight_layout() st.pyplot(fig1) # Payback Period Chart st.subheader("System Payback Period") # Fix large values filtered_fin_df["payback_period_fixed"] = filtered_fin_df[ "payback_period" ].clip(0, 30) fig2, ax2 = plt.subplots(figsize=(12, 6)) sns.barplot( data=filtered_fin_df, x="Scenario", y="payback_period_fixed", palette="rocket_r", ax=ax2, ) ax2.set_title("Investment Payback Period by Scenario") ax2.set_ylabel("Years") plt.xticks(rotation=45, ha="right") plt.tight_layout() st.pyplot(fig2) # Financial summary metrics col1, col2, col3 = st.columns(3) with col1: avg_savings = filtered_fin_df["monthly_savings"].mean() st.metric( "Avg. Monthly Savings", f"{avg_savings:,.0f} Ksh", ( f"{avg_savings - df['monthly_savings'].mean():,.0f} Ksh" if avg_savings > df["monthly_savings"].mean() else f"{avg_savings - df['monthly_savings'].mean():,.0f} Ksh" ), ) with col2: min_payback = filtered_fin_df["payback_period"].min() st.metric( "Best Payback Period", f"{min_payback:.1f} years", help="Shortest time to recover investment", ) with col3: total_investment = ( st.session_state.solar_panels * st.session_state.panel_price + st.session_state.batteries * st.session_state.battery_price ) annual_roi = ( (avg_savings * 12 / total_investment) * 100 if total_investment > 0 else 0 ) st.metric( "Annual ROI", f"{annual_roi:.1f}%", help="Annual Return on Investment" ) with st.expander("💵 Financial Analysis Details"): st.markdown( f""" **Investment Details:** - Total Solar Panel Investment: {st.session_state.solar_panels:,} panels × {st.session_state.panel_price:,} Ksh = {st.session_state.solar_panels * st.session_state.panel_price:,} Ksh - Total Battery Investment: {st.session_state.batteries:,} batteries × {st.session_state.battery_price:,} Ksh = {st.session_state.batteries * st.session_state.battery_price:,} Ksh - Total System Cost: {total_investment:,} Ksh **Savings Calculation:** - Grid Price: {st.session_state.grid_price} Ksh/kWh - Monthly Savings =(Total Consumption - Common Area) × Grid Price - Payback Period = Total Investment / Annual Savings **Filtered Scenario Data:** """ ) st.dataframe( filtered_fin_df[ [ "Scenario", "consumption", "production", "monthly_savings", "payback_period", ] ].sort_values("monthly_savings", ascending=False), hide_index=True, ) # Button to trigger analysis if st.button("🔍 Analyze Financial Data with LLM"): with st.spinner("Generating insights with AI..."): analysis = summary_generation(filtered_fin_df) st.success("Analysis Complete!") st.write(analysis) # Display the results # Tab 3: Detailed Breakdown with tab3: st.header("Consumption Breakdown Analysis") # Select specific scenario for detailed analysis scenario_select = st.selectbox( "Select Specific Scenario", list(scenarios.keys()) ) selected_params = scenarios[scenario_select] # Create consumption breakdown breakdown_df = create_consumption_breakdown( selected_params["1br"], selected_params["2br"], selected_params["common"] ) total_kwh = breakdown_df["kWh"].sum() # Add percentage column breakdown_df["Percentage"] = (breakdown_df["kWh"] / total_kwh * 100).round(1) col1, col2 = st.columns([2, 3]) with col1: st.subheader("Energy Composition") # Create a more attractive pie chart fig3 = plt.figure(figsize=(8, 8)) ax3 = fig3.add_subplot(111) colors = ["#FF9800", "#2196F3", "#4CAF50"] explode = (0.1, 0, 0) wedges, texts, autotexts = ax3.pie( breakdown_df["kWh"], labels=breakdown_df.index, autopct="%1.1f%%", explode=explode, colors=colors, shadow=True, startangle=90, textprops={"fontsize": 12}, ) # Equal aspect ratio ensures that pie is drawn as a circle ax3.axis("equal") plt.tight_layout() st.pyplot(fig3) # Show total consumption st.metric( "Total Monthly Consumption", f"{total_kwh:.1f} kWh", help="Sum of all consumption components", ) with col2: st.subheader("Detailed Component Analysis") # Show breakdown as a horizontal bar chart fig4 = plt.figure(figsize=(10, 5)) ax4 = fig4.add_subplot(111) # Sort by consumption sorted_df = breakdown_df.sort_values("kWh", ascending=True) # Create horizontal bar chart bars = sns.barplot( y=sorted_df.index, x="kWh", data=sorted_df, palette=colors[::-1], ax=ax4 ) # Add data labels for i, v in enumerate(sorted_df["kWh"]): ax4.text( v + 5, i, f"{v:.1f} kWh ({sorted_df['Percentage'].iloc[i]}%)", va="center", ) ax4.set_title(f"Energy Consumption Breakdown - {scenario_select}") ax4.set_xlabel("Monthly Consumption (kWh)") ax4.set_ylabel("") plt.tight_layout() st.pyplot(fig4) # Add scenario details st.markdown( f""" **Scenario Details:** - 1BR Units Occupancy: {selected_params['1br']*100:.0f}% ({selected_params['1br']*ONE_BR_UNITS:.0f} units) - 2BR Units Occupancy: {selected_params['2br']*100:.0f}% ({selected_params['2br']*TWO_BR_UNITS:.0f} units) - Common Areas Consumption: {selected_params['common']*30:.1f} kWh/month """ ) # Insight box st.info( f""" **Key Insights for {scenario_select}:** - Lighting contributes {breakdown_df.loc['Lighting', 'Percentage']:.1f}% of total consumption - Common areas account for {breakdown_df.loc['Common Areas', 'Percentage']:.1f}% of the total - {'2BR units dominate consumption at ' + str(selected_params['2br']*100) + '% occupancy' if selected_params['2br'] > selected_params['1br'] else '1BR units are the primary consumers at ' + str(selected_params['1br']*100) + '% occupancy'} - Total potential solar offset: {min(solar_production(st.session_state.solar_panels)/total_kwh*100, 100):.1f}% """ ) # Footer st.markdown("---") st.markdown( """

Solar Analysis Suite v1.0 | Developed with ❤️ for sustainable energy solutions

""", unsafe_allow_html=True, ) if __name__ == "__main__": main()