solar_savings / app.py
charagu-eric's picture
returned st
c0632d6
# 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(
"""
<style>
.main .block-container {padding-top: 2rem;}
h1, h2, h3 {color: #1E88E5;}
.stExpander {border-radius: 8px; border: 1px solid #1E88E5;}
.stTabs [data-baseweb="tab-list"] {gap: 10px;}
.stTabs [data-baseweb="tab"] {
height: 50px;
white-space: pre-wrap;
background-color: #F0F2F6;
border-radius: 4px 4px 0px 0px;
gap: 1px;
padding-top: 10px;
padding-bottom: 10px;
}
.stTabs [aria-selected="true"] {
background-color: #1E88E5;
color: white;
}
</style>
""",
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(
"""
<div style="text-align: center; color: #666;">
<p>Solar Analysis Suite v1.0 | Developed with ❤️ for sustainable energy solutions</p>
</div>
""",
unsafe_allow_html=True,
)
if __name__ == "__main__":
main()