{ "cells": [ { "cell_type": "markdown", "id": "cb6f457b-af3e-4804-b083-6c204ec3274c", "metadata": {}, "source": [ "# Importar bibliotecas" ] }, { "cell_type": "code", "execution_count": null, "id": "33e95515-990b-4087-9ed4-9eca01c5e541", "metadata": {}, "outputs": [], "source": [ "import os\n", "import numpy as np \n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "import matplotlib.dates as mdates\n", "import plotly.graph_objects as go\n", "import pvlib\n", "import warnings" ] }, { "cell_type": "markdown", "id": "59f16b06-d7cb-4aa7-a165-85e50e05d6b5", "metadata": {}, "source": [ "**Nota importante: não alterar o nome de variáveis ou de colunas, para que o código mais à frente não deixe de funcionar.**\n", "\n", "Uma boa parte do código já está preparada. Deves completar onde estão os comentários `# TODO`, seguindo as indicações." ] }, { "cell_type": "markdown", "id": "eb698f8a-9fbc-4ea6-a631-8ea1c0628ce5", "metadata": {}, "source": [ "# Exercícios" ] }, { "cell_type": "markdown", "id": "1c55cb57-3f74-43b0-8359-bd2ae9771c07", "metadata": {}, "source": [ "## Parte A: Solar fotovoltaico" ] }, { "cell_type": "markdown", "id": "b5afca93-123b-4625-9c43-eef1290e6579", "metadata": {}, "source": [ "### **1.** Dados de consumo" ] }, { "cell_type": "markdown", "id": "463232d2-8d24-487f-97b7-279c6722d0db", "metadata": {}, "source": [ "Vai ao [balcão digital da E-Redes](https://balcaodigital.e-redes.pt/home) e faz login. Vai a \"Os meus locais\", depois \"Produção, consumos e potências\", e por fim \"Consultar histórico\". Depois de selecionar o local, vais ver um gráfico do consumo da tua casa este mês, até hoje.\n", "\n", "O objetivo aqui é conseguir um ano inteiro de dados. Preferencialmente de 2023, mas, se não tiveres dados disponíveis para o ano todo, e tiveres de outro ano, podes usar de outro ano.\n", "\n", "Seleciona um mês de cada vez e faz download com \"Exportar excel\" até conseguires um ano inteiro.\n", "\n", "Dentro da pasta onde tens o notebook, cria outra pasta com o nome \"consumos\". Coloca todos os ficheiros de dados dentro dessa pasta." ] }, { "cell_type": "code", "execution_count": null, "id": "b69c281a-74a2-4808-9e75-9ae814ad8fda", "metadata": {}, "outputs": [], "source": [ "filenames = os.listdir('consumos')\n", "warnings.filterwarnings(\"ignore\", category=UserWarning, module=\"openpyxl.styles.stylesheet\")\n", "\n", "for i, filename in enumerate(filenames):\n", " df_month = pd.read_excel(os.path.join('consumos', filename), skiprows=8)\n", " if i == 0:\n", " df_cons = df_month\n", " else:\n", " df_cons = pd.concat([df_cons, df_month])\n", " \n", "df_cons['Datetime'] = pd.to_datetime(df_cons['Data'] + ' ' + df_cons['Hora'], format='%Y/%m/%d %H:%M')\n", "df_cons = df_cons.drop(['Data', 'Hora', 'Estado'], axis=1).set_index('Datetime').rename({'Consumo registado (kW)': 'Consumo (kW)'}, axis=1)\n", "df_cons = df_cons[~((df_cons.index.month == 2) & (df_cons.index.day == 29))]\n", "df_cons.index = df_cons.index.map(lambda x: x.replace(year=2023))\n", "\n", "# a mudança de hora no outono gera duplicados. removemos fazendo a média de ambos.\n", "df_cons = df_cons.groupby(df_cons.index).mean()\n", "\n", "df_cons" ] }, { "cell_type": "code", "execution_count": null, "id": "668ae520-e2b5-4dfd-b7f9-1a361961da45", "metadata": {}, "outputs": [], "source": [ "if sorted(df_cons.index.month.unique().to_list()) != list(np.arange(1,13)):\n", " print(\"Atenção: os teus dados não têm um ano inteiro.\")" ] }, { "cell_type": "markdown", "id": "09881821-54fb-4b22-a98c-55af9f5581de", "metadata": {}, "source": [ "Em alternativa, se não conseguires aceder aos teus próprios dados de consumo, faz download do ficheiro de consumos disponível no Fénix, coloca-o na mesma pasta que o notebook, e descomenta o código abaixo:" ] }, { "cell_type": "code", "execution_count": null, "id": "2c3c7c65-6a0e-43a6-a88f-5db1fc391ad9", "metadata": {}, "outputs": [], "source": [ "# df_cons = pd.read_csv('consumo_backup.csv')[['Total Consumption']]\n", "# df_cons.columns = ['Consumo (kW)']\n", "# df_cons.index = pd.date_range(start='2020-1-1', periods=len(df_cons), freq='15min')\n", "# df_cons = df_cons[~((df_cons.index.month == 2) & (df_cons.index.day == 29))]\n", "# df_cons.index = df_cons.index.map(lambda x: x.replace(year=2023))\n", "# df_cons" ] }, { "cell_type": "markdown", "id": "0d38026b-76d4-4ba4-ac73-d15f3e1c7103", "metadata": {}, "source": [ "### **2.** Utilizando o `pvlib`, cria uma dataframe de dados de geração de um sistema PV de 1 kWp, 30 graus de inclinação e orientado a sul, para a localização da casa em análise." ] }, { "cell_type": "markdown", "id": "3c2b4711-300c-46fe-8189-f7974beb342d", "metadata": {}, "source": [ "Vamos agora obter dados de geração PV para um sistema de 1 kW, usando o ``pvlib``, mais concretamente a função ``pvlib.iotools.get_pvgis_hourly``. Podes consultar a documentação desta função [aqui](https://pvlib-python.readthedocs.io/en/v0.11.2/reference/generated/pvlib.iotools.get_pvgis_hourly.html)." ] }, { "cell_type": "code", "execution_count": null, "id": "bf0e8b81-df15-4b6d-9c58-65564d2c62db", "metadata": {}, "outputs": [], "source": [ "# TODO: insere as coordenadas aproximadas da tua casa\n", "latitude = \n", "longitude = \n", "\n", "inclinacao = 30\n", "azimute = 180\n", "\n", "df_pvgis, _, _ = pvlib.iotools.get_pvgis_hourly(latitude, longitude,\n", " surface_tilt=inclinacao,\n", " surface_azimuth=azimute,\n", " pvcalculation=True,\n", " peakpower=1,\n", " pvtechchoice='crystSi',\n", " mountingplace='building',\n", " start='2023')\n", "df_pvgis = df_pvgis[['P']].tz_localize(None).rename({'P': 'Geração (kW)'}, axis=1) / 1000\n", "df_pvgis.head(24)" ] }, { "cell_type": "markdown", "id": "b2ed1fb5-422f-43ee-9f3d-52169811c333", "metadata": {}, "source": [ "A resolução destes dados é horária (e sempre aos 10 minutos de cada hora), o que não bate certo com os dados de consumo com resolução de 15 minutos. Por isso vamos interpolar os dados para alterar a sua resolução." ] }, { "cell_type": "code", "execution_count": null, "id": "9c0fbf13-b65f-4b7e-ba38-c34ac73261c1", "metadata": {}, "outputs": [], "source": [ "new_index = pd.date_range(start=df_pvgis.index.min().floor(\"h\"), \n", " end=df_pvgis.index.max().ceil(\"h\"), \n", " freq=\"15min\")\n", "df_pvgis = df_pvgis.reindex(df_pvgis.index.union(new_index))\n", "df_pvgis = df_pvgis.interpolate(method=\"time\")\n", "df_pvgis = df_pvgis.reindex(new_index).bfill()\n", "df_pvgis" ] }, { "cell_type": "markdown", "id": "4a844eb5-6aa6-43cb-b351-a82f6eb51c2b", "metadata": {}, "source": [ "#### a) Qual a geração total deste sistema ao longo do ano?" ] }, { "cell_type": "code", "execution_count": null, "id": "3d9322bd-7596-4856-a523-118400da8af5", "metadata": {}, "outputs": [], "source": [ "geracao_total = # TODO: calcula a geração total anual\n", "print(f\"Geração anual: {round(geracao_total)} kWh\")" ] }, { "cell_type": "markdown", "id": "1e87122e-18e3-4286-9254-df1b2b4e61eb", "metadata": {}, "source": [ "#### b) Apresenta um gráfico com a geração solar e o consumo de um dia à tua escolha." ] }, { "cell_type": "markdown", "id": "8fd10ba9-88f3-4fa3-9a03-68d2433ac85a", "metadata": {}, "source": [ "Vamos juntar os dados numa só dataframe." ] }, { "cell_type": "code", "execution_count": null, "id": "0db6ce41-4f09-438f-92aa-2a6255ebcb7a", "metadata": {}, "outputs": [], "source": [ "df = pd.concat([df_cons, df_pvgis], axis=1)\n", "df" ] }, { "cell_type": "code", "execution_count": null, "id": "ebfe2ccf-c4a3-4839-ab57-b7d94c4f78cb", "metadata": {}, "outputs": [], "source": [ "dia = # TODO: escolhe um dia e insere a data no formato \"ano-mês-dia\". Podes experimentar diferentes dias, em diferentes estações, e ver se observas padrões diferentes :)\n", "\n", "df_dia = df.loc[dia]\n", "\n", "plt.plot(df_dia['Consumo (kW)'], label='Consumo')\n", "plt.plot(df_dia['Geração (kW)'], label='Geração')\n", "\n", "plt.gca().xaxis.set_major_formatter(mdates.DateFormatter(\"%H:%M\"))\n", "\n", "plt.xlabel('Hora do dia')\n", "plt.ylabel('Potência (kW)')\n", "\n", "plt.xlim([df_dia.index.min(), df_dia.index.max()])\n", "plt.ylim(bottom=0)\n", "\n", "plt.grid()\n", "plt.legend()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "25940843-091f-4b98-bc55-b2395dc02db9", "metadata": {}, "source": [ "### **3.** Calcula o autoconsumo e a autossuficiência." ] }, { "cell_type": "markdown", "id": "f74b8183-383b-463f-9214-77bbb8fdb34a", "metadata": {}, "source": [ "Para ser autoconsumida, a energia tem de ser consumida no mesmo período de 15 minutos em que é produzida.\n", "\n", "Se o consumo for superior à geração, então toda a geração é autoconsumida, e o resto da energia necessária para cobrir o consumo vem da rede.\n", "\n", "Se o consumo for inferior à geração, então a geração cobre a totalidade do consumo, e o excesso de geração é injetado na rede." ] }, { "cell_type": "code", "execution_count": null, "id": "56befad2-2b26-4f6b-afda-bcfa1d6029f7", "metadata": {}, "outputs": [], "source": [ "df['Autoconsumido (kW)'] = np.minimum(df['Consumo (kW)'], df['Geração (kW)'])\n", "df" ] }, { "cell_type": "code", "execution_count": null, "id": "93e82ed1-d5a7-43e7-8f40-4cfef34ba252", "metadata": {}, "outputs": [], "source": [ "geracao_total = df['Geração (kW)'].sum() * 15/60\n", "consumo_total = # TODO: calcula o consumo total anual\n", "autoconsumido_total = # TODO: calcula a energia autoconsumida total anual\n", "\n", "print(f\"Geração total: {round(geracao_total)} kWh\")\n", "print(f\"Consumo total: {round(consumo_total)} kWh\")\n", "print(f\"Total de geração autoconsumida: {round(autoconsumido_total)} kWh\")" ] }, { "cell_type": "code", "execution_count": null, "id": "feaaabbb-1196-46b2-a335-e0398a8e3718", "metadata": {}, "outputs": [], "source": [ "autoconsumo = # TODO: calcula o autoconsumo\n", "print(f\"Autoconsumo: {round(autoconsumo)} %\")" ] }, { "cell_type": "code", "execution_count": null, "id": "6a1467fd-f045-4feb-ad89-db10080b00ea", "metadata": {}, "outputs": [], "source": [ "autosuficiencia = # TODO: calcula a autossuficiência\n", "print(f\"Autossuficiência: {round(autosuficiencia)} %\")" ] }, { "cell_type": "markdown", "id": "70442b23-d69c-4c7c-8e33-b6c91df18842", "metadata": {}, "source": [ "### **4.** Apresenta um gráfico comparando os perfis médios de consumo e de geração. Quais costumam ser as horas de maior consumo? Concidem com as horas de maior geração? Quais são as implicações disto?" ] }, { "cell_type": "code", "execution_count": null, "id": "8216335d-b01b-4369-8f7a-2ffc04749337", "metadata": {}, "outputs": [], "source": [ "# Create \"Time of Day\" column as hours (convert time object to a number)\n", "df['hour_decimal'] = df.index.hour + df.index.minute / 60 # e.g., 14:30 → 14.5\n", "\n", "# Group by the 15-minute average daily profile\n", "avg_load_profile = df.groupby('hour_decimal')['Consumo (kW)'].mean()\n", "avg_pv_profile = df.groupby('hour_decimal')['Geração (kW)'].mean()\n", "\n", "df = df.drop('hour_decimal', axis=1)\n", "\n", "# Plot\n", "plt.figure(figsize=(10, 5))\n", "plt.plot(avg_load_profile.index, avg_load_profile, label='Perfil de consumo médio', linewidth=2)\n", "plt.plot(avg_pv_profile.index, avg_pv_profile, label='Perfil de geração PV médio', linewidth=2)\n", "\n", "plt.xlim([avg_load_profile.index.min(), avg_load_profile.index.max()])\n", "plt.ylim(bottom=0)\n", "\n", "# Formatting\n", "plt.xlabel('Hora do dia')\n", "plt.ylabel('Potência (kW)')\n", "plt.title('Comparação de perfis médios')\n", "plt.xticks(range(0, 25, 2)) # Show every 2 hours\n", "plt.legend()\n", "plt.grid()\n", "\n", "# Show plot\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "3ecc2248-388e-4f50-a24c-695f96fa8867", "metadata": {}, "source": [ "### **5.** Assumindo um tarifa fixa de eletricidade de 0.18€/kWh, quais as poupanças anuais na fatura da eletricidade que resultam do sistema fotovoltaico?" ] }, { "cell_type": "code", "execution_count": null, "id": "02280eb5-c6cd-4f40-a102-9e3adf74ec3c", "metadata": {}, "outputs": [], "source": [ "tarifa = 0.18\n", "\n", "poupanca = # TODO: calcula a poupança anual\n", "\n", "print(f\"Poupança anual: {round(poupanca, 2)} €\")" ] }, { "cell_type": "markdown", "id": "4cb2a3ce-9c90-4f79-8034-7734485f28cc", "metadata": {}, "source": [ "### **6.** Testa o impacto de sistemas fotovoltaicos com diferentes configurações (potência, inclinação e azimute) no perfil médio de geração, e em como este se alinha com o teu perfil de consumo médio." ] }, { "cell_type": "markdown", "id": "95f83e61-e625-413c-9578-c5ffbb1cc07e", "metadata": {}, "source": [ "Até agora, fizemos contas com um sistema de 1 kWp horizontal. Vamos experimentar diferentes inclinações e potências instaladas.\n", "\n", "Começamos por criar uma função que repete todas as contas que fizemos anteriormente, aceitando como argumento as coordenadas do local, a inclinação, azimute, e potência instalada do sistema fotovoltaico, e a dataframe do consumo. Devolve a geração total, o autoconsumo e a autossuficiência calculadas, bem como o perfil médio diário de geração." ] }, { "cell_type": "code", "execution_count": null, "id": "8973ce97-82b9-43c0-b7ef-f3cf1fd45b37", "metadata": {}, "outputs": [], "source": [ "def calcular_autoconsumo_e_autosuficiencia(latitude, longitude, inclinacao, azimute, potencia, df_cons):\n", " df_pvgis, _, _ = pvlib.iotools.get_pvgis_hourly(latitude, longitude,\n", " surface_tilt=inclinacao,\n", " surface_azimuth=azimute,\n", " pvcalculation=True,\n", " peakpower=potencia,\n", " pvtechchoice='crystSi',\n", " mountingplace='building',\n", " start='2023')\n", " \n", " df_pvgis = df_pvgis[['P']].tz_localize(None).rename({'P': 'Geração (kW)'}, axis=1) / 1000\n", " \n", " new_index = pd.date_range(start=df_pvgis.index.min().floor(\"h\"), \n", " end=df_pvgis.index.max().ceil(\"h\"), \n", " freq=\"15min\")\n", " df_pvgis = df_pvgis.reindex(df_pvgis.index.union(new_index))\n", " df_pvgis = df_pvgis.interpolate(method=\"time\")\n", " df_pvgis = df_pvgis.reindex(new_index).bfill()\n", " \n", " df = pd.concat([df_cons, df_pvgis], axis=1)\n", "\n", " df['Autoconsumido (kW)'] = np.minimum(df['Consumo (kW)'], df['Geração (kW)'])\n", " geracao_total = df['Geração (kW)'].sum() * 15/60\n", " consumo_total = df['Consumo (kW)'].sum() * 15/60\n", " autoconsumido_total = df['Autoconsumido (kW)'].sum() * 15/60\n", " \n", " autoconsumo = autoconsumido_total / geracao_total * 100\n", " autosuficiencia = autoconsumido_total / consumo_total * 100\n", "\n", " # Compute the average daily profile\n", " df['Hora do Dia'] = df.index.hour + df.index.minute / 60\n", " perfil_diario = df.groupby('Hora do Dia')[['Geração (kW)']].mean().reset_index()\n", "\n", " return geracao_total, autoconsumo, autosuficiencia, perfil_diario" ] }, { "cell_type": "markdown", "id": "8953cca4-8d6f-4bc9-b876-56e0c07e16e6", "metadata": {}, "source": [ "Os valores a testar para cada variável estão definidos na célula abaixo:" ] }, { "cell_type": "code", "execution_count": null, "id": "ddc7816a-cd73-42a8-86c3-144510e21698", "metadata": {}, "outputs": [], "source": [ "inclinacoes = [0, 30, 55]\n", "azimutes = [90, 180, 270]\n", "potencias = [0.25, 0.5, 1, 2, 5, 10]" ] }, { "cell_type": "markdown", "id": "82a38a4f-db55-4167-97f9-058cd24ab213", "metadata": {}, "source": [ "A célula seguinte recebe os dados do PVGIS e faz os cálculos para cada uma das combinações. É normal que demore alguns minutos a correr." ] }, { "cell_type": "code", "execution_count": null, "id": "d6fea24c-029c-42b9-9a61-aa92aa8d0fdc", "metadata": {}, "outputs": [], "source": [ "rows = []\n", "daily_profiles = [] # Store profiles separately\n", "\n", "for inclinacao in inclinacoes:\n", " for azimute in azimutes:\n", " for potencia in potencias:\n", " geracao_total, autoconsumo, autosuficiencia, daily_profile = calcular_autoconsumo_e_autosuficiencia(latitude, longitude, inclinacao, azimute, potencia, df_cons)\n", " \n", " # Store summary results\n", " rows.append({\n", " 'Inclinação': inclinacao,\n", " 'Azimute': azimute,\n", " 'Potência': potencia,\n", " 'Autoconsumo': autoconsumo,\n", " 'Autossuficiência': autosuficiencia,\n", " 'Geração total': geracao_total\n", " })\n", "\n", " # Store daily profile with scenario identifiers\n", " daily_profile['Inclinação'] = inclinacao\n", " daily_profile['Azimute'] = azimute\n", " daily_profile['Potência'] = potencia\n", " daily_profiles.append(daily_profile)\n", "\n", "# Convert results to DataFrames\n", "df_results = pd.DataFrame(rows)\n", "df_daily_profiles = pd.concat(daily_profiles, ignore_index=True)" ] }, { "cell_type": "code", "execution_count": null, "id": "e146a77a-1b09-409e-876f-be6f1fd36db2", "metadata": { "scrolled": true }, "outputs": [], "source": [ "df_results" ] }, { "cell_type": "markdown", "id": "46e35c30-df51-4974-b150-2a18411e5794", "metadata": {}, "source": [ "Vamos observar a variação do perfil de geração com a inclinação e azimute. Para isto, vamos fixar a potência. Podes ajustar este valor ao que te parecer mais adequado para o teu caso." ] }, { "cell_type": "code", "execution_count": null, "id": "94d2d9a8-f776-4065-9dac-f305aff34b09", "metadata": {}, "outputs": [], "source": [ "potencia = 1" ] }, { "cell_type": "markdown", "id": "d9a3cf63-fbd6-4e63-ad98-9db7217fb2a6", "metadata": {}, "source": [ "O gráfico produzido pela célula seguinte é interativo. Usando os menus drop-down, \n", "\n", "**a)** Fixa o azimute nos 180º. Como varia a geração com a variação da inclinação?\n", "\n", "**b)** Fixa a inclinação nos 30º. O que observas relativamente ao impacto do azimute? Qual te parece ser o melhor azimute para o teu caso? Em que situações seriam melhores outros?" ] }, { "cell_type": "code", "execution_count": null, "id": "5a1c6b36-438c-4139-889d-cbd0f565239e", "metadata": {}, "outputs": [], "source": [ "# Filter dataset to only include Potência = 1 kW\n", "df_filtered = df_daily_profiles[df_daily_profiles['Potência'] == potencia]\n", "\n", "# Unique values for dropdowns\n", "inclinacoes = df_filtered['Inclinação'].unique()\n", "azimutes = df_filtered['Azimute'].unique()\n", "\n", "# Store traces for different scenarios\n", "traces = []\n", "\n", "# PV Generation traces for different scenarios\n", "for (inclinacao, azimute), group in df_filtered.groupby(['Inclinação', 'Azimute']):\n", " traces.append(go.Scatter(\n", " x=group['Hora do Dia'], \n", " y=group['Geração (kW)'], \n", " mode='lines',\n", " name=f\"Geração - Inclinação {inclinacao}°, Azimute {azimute}°\",\n", " legendgroup=f\"{inclinacao}-{azimute}\",\n", " visible=True # Start with all traces visible\n", " ))\n", "\n", "# Load Consumption trace (same for all scenarios, always visible)\n", "load_profile_trace = go.Scatter(\n", " x=avg_load_profile.index, \n", " y=avg_load_profile.values, \n", " mode='lines',\n", " name=\"Consumo Médio\",\n", " line=dict(dash='dash', color='black'), # Dashed black line for consumption\n", " legendgroup=\"Consumo (kW)\"\n", ")\n", "traces.append(load_profile_trace) # Append at the end\n", "\n", "# Add traces to figure\n", "fig = go.Figure(data=traces)\n", "\n", "# Function to generate dropdown options\n", "def generate_dropdown_options(values, column_name):\n", " options = [{'label': \"All\", 'method': \"update\", \n", " 'args': [{\"visible\": [True] * (len(traces) - 1) + [True]}]}] # Always keep the last trace (load profile) visible\n", " \n", " for value in values:\n", " visibility = [\n", " (trace.legendgroup.startswith(f\"{value}-\") if column_name == \"Inclinação\" else\n", " trace.legendgroup.split('-')[1] == str(value) if trace.legendgroup != \"Consumo\" else True)\n", " for trace in traces[:-1] # Exclude the last trace (load profile) from filtering\n", " ]\n", " visibility.append(True) # Ensure load profile stays visible\n", " options.append({'label': f\"{column_name} {value}\", 'method': \"update\", 'args': [{\"visible\": visibility}]})\n", " \n", " return options\n", "\n", "# Add dropdown menus\n", "fig.update_layout(\n", " updatemenus=[\n", " dict(\n", " buttons=generate_dropdown_options(inclinacoes, \"Inclinação\"),\n", " direction=\"down\", \n", " x=0.15, xanchor=\"center\", \n", " y=1.05, yanchor=\"top\"\n", " ),\n", " dict(\n", " buttons=generate_dropdown_options(azimutes, \"Azimute\"),\n", " direction=\"down\", \n", " x=0.35, xanchor=\"center\", \n", " y=1.05, yanchor=\"top\"\n", " )\n", " ],\n", " title=\"Comparação de Perfis Diários de Geração PV vs. Consumo (1 kW)\",\n", " title_x=0.5, # Center the title\n", " xaxis_title=\"Hora do Dia\",\n", " yaxis_title=\"Energia (kW)\",\n", " template=\"plotly_white\",\n", " width=1200,\n", " height=700,\n", " \n", " # Set x-axis ticks every 3 units (based on your data)\n", " xaxis=dict(\n", " tickmode='array',\n", " tickvals=[i for i in range(0, len(df_filtered), 3)], # Every 3rd unit\n", " ticktext=[f\"{i}\" for i in range(0, len(df_filtered), 3)] # Adjust labels accordingly\n", " )\n", ")\n", "\n", "fig.show()\n" ] }, { "cell_type": "markdown", "id": "922d61d4-7a5b-42cc-ba97-65f0a72995dd", "metadata": {}, "source": [ "### **7.** Como variam o autoconsumo e autossuficiência com a variação da potência instalada do sistema fotovoltaico?" ] }, { "cell_type": "markdown", "id": "e317c864-771d-4719-bdb5-144926f3a7b1", "metadata": {}, "source": [ "Observa o gráfico seguinte. Como varia o autoconsumo com o aumento da potência instalada?" ] }, { "cell_type": "code", "execution_count": null, "id": "7f580966-cff4-48ea-8d8a-7dfe18197730", "metadata": {}, "outputs": [], "source": [ "plt.scatter(df_results[\"Potência\"], df_results[\"Autoconsumo\"], alpha=0.7)\n", "\n", "plt.xlabel(\"Potência (kW)\")\n", "plt.ylabel(\"Autoconsumo (%)\")\n", "\n", "plt.xlim(left=0)\n", "\n", "plt.grid()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "4630ef15-e466-44ca-b0a2-f6432d4f2f58", "metadata": {}, "source": [ "Reproduz o gráfico anterior, desta vez para a autossuficiência. Como varia?" ] }, { "cell_type": "code", "execution_count": null, "id": "71c887a5-d014-4a5d-8487-1142649c5739", "metadata": {}, "outputs": [], "source": [ "# TODO: gráfico da autossuficiência em função da potência instalada" ] }, { "cell_type": "markdown", "id": "b1b48ecd-fd10-44af-98f9-aeb362b06f9e", "metadata": {}, "source": [ "### **8.** O que poderias fazer, a nível de comportamentos/consumos, para aumentar o teu autoconsumo e autossuficiência?" ] }, { "cell_type": "markdown", "id": "1b71dfb6-477a-4524-8d09-4e127af1497a", "metadata": {}, "source": [ "## Parte B: Solar térmico" ] }, { "cell_type": "markdown", "id": "e7bc4b47-7480-46de-9f99-d0e9fbedf9d7", "metadata": {}, "source": [ "### **1.** Calcula:" ] }, { "cell_type": "markdown", "id": "0877ab32-7918-4778-a64f-d25393425fb3", "metadata": {}, "source": [ "#### a) a energia útil necessária para que 40 l de água a 15ºC atinjam uma temperatura de 50ºC." ] }, { "cell_type": "code", "execution_count": null, "id": "908957a9-ad57-4f0b-8869-784d8aecb513", "metadata": {}, "outputs": [], "source": [ "E = # TODO: calcular a energia útil\n", "\n", "print(f\"Energia útil: {round(E)} J\")" ] }, { "cell_type": "markdown", "id": "c2ce3724-29a2-4684-aeba-219cdb511986", "metadata": {}, "source": [ "#### b) o rendimento de um coletor plano com parâmetros característicos $\\eta_0 = 0.8$, $\\alpha_1 = 7$, e $\\alpha_2 = 0.014$ para as temperaturas da alínea anterior, e uma irradiância média de $G = 1000$ W/m$^2$. Qual dos dois tem maior impacto: as perdas óticas, ou as perdas térmicas?" ] }, { "cell_type": "code", "execution_count": null, "id": "f9ec985d-6398-4252-862e-43f1015f8760", "metadata": {}, "outputs": [], "source": [ "# TODO: calcular\n", "\n", "perdas_oticas = \n", "perdas_termicas = \n", "\n", "eficiencia = \n", "\n", "print(f\"Perdas óticas: {round(perdas_oticas, 3)}\")\n", "print(f\"Perdas térmicas: {round(perdas_termicas, 3)}\")\n", "print(f\"Eficiência: {round(eficiencia, 3)}\")" ] }, { "cell_type": "markdown", "id": "41c38b5d-ef05-4b3b-b122-9d3054b56979", "metadata": {}, "source": [ "### **2.** Calcula, para cada dia do ano, a energia diária fornecida por um coletor plano de 1 m2, em posição horizontal, com os parâmetros da questão anterior. Para fins de cálculo da eficiência, assume uma irradiância média de 700 W/m2." ] }, { "cell_type": "markdown", "id": "cc9d0873-9245-4058-bd80-3fdcb58c681f", "metadata": {}, "source": [ "Começamos por receber dados do PVGIS para a localização da casa em análise." ] }, { "cell_type": "code", "execution_count": null, "id": "ea20eaf2-a521-4163-b205-7637e523dcbe", "metadata": {}, "outputs": [], "source": [ "df_, _, _ = pvlib.iotools.get_pvgis_hourly(latitude, longitude, start=2023, end=2023)\n", "df_['poa_total'] = df_['poa_direct'] + df_['poa_sky_diffuse']\n", "df_" ] }, { "cell_type": "markdown", "id": "ce6910a8-7591-4d98-a9b7-c62bad6b7979", "metadata": {}, "source": [ "Vamos agregar os dados por dia, calculando a soma no caso da radiação, e a média no caso da temperatura." ] }, { "cell_type": "code", "execution_count": null, "id": "df2e6465-c9a7-49d8-a751-68feeca66d1c", "metadata": {}, "outputs": [], "source": [ "df_diaria = df_.resample('D').agg({'poa_total': 'sum',\n", " 'temp_air': 'mean'})\n", "\n", "df_diaria['poa_total'] = df_diaria['poa_total']/1000\n", "\n", "df_diaria = df_diaria.rename(columns={'poa_total': 'Irradiação (kWh/m2)', \n", " 'temp_air': 'Temperatura (ºC)'})\n", "\n", "df_diaria.head()" ] }, { "cell_type": "code", "execution_count": null, "id": "405c3ee6-b273-49bc-8dd1-2c00daf7b7e6", "metadata": {}, "outputs": [], "source": [ "fig, ax1 = plt.subplots(figsize=(10, 6))\n", "\n", "# Plot Total Irradiation on the first y-axis (left)\n", "ax1.set_xlabel(\"Dia\")\n", "ax1.set_ylabel(\"Irradiação (kWh/m2)\", color=\"tab:orange\")\n", "ax1.plot(df_diaria.index, df_diaria[\"Irradiação (kWh/m2)\"], color=\"tab:orange\", label=\"Irradiação (kWh/m2)\")\n", "ax1.tick_params(axis=\"y\", labelcolor=\"tab:orange\")\n", "ax1.grid(True, which='both', linestyle='--', linewidth=0.5)\n", "\n", "# Create a second y-axis (right) for Temperature\n", "ax2 = ax1.twinx()\n", "ax2.set_ylabel(\"Temperatura Média (°C)\", color=\"tab:blue\")\n", "ax2.plot(df_diaria.index, df_diaria[\"Temperatura (ºC)\"], color=\"tab:blue\", label=\"Temperatura Média (°C)\")\n", "ax2.tick_params(axis=\"y\", labelcolor=\"tab:blue\")\n", "\n", "plt.xlim([df_pvgis.index.min(), df_pvgis.index.max()])\n", "\n", "ax1.xaxis.set_major_locator(mdates.MonthLocator()) # Monthly ticks\n", "ax1.xaxis.set_major_formatter(mdates.DateFormatter('%b %d')) # Format: \"Jan 01\", \"Feb 01\", etc.\n", "\n", "# Rotate x-tick labels for better readability\n", "for label in ax1.get_xticklabels():\n", " label.set_rotation(90)\n", " label.set_ha(\"right\")\n", " \n", "# Show the plot\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "695bd352-cf64-40bb-b1c0-59a3f31305c9", "metadata": {}, "source": [ "O primeiro passo dos nossos cálculos é calcular a eficiência média do coletor em cada dia do ano. Criamos uma função que calcule a eficiência com base nos parâmetros necessários, e aplicamo-la aos nossos dados." ] }, { "cell_type": "code", "execution_count": null, "id": "c320ef00-7c36-444f-b553-2b8197bfd9bd", "metadata": {}, "outputs": [], "source": [ "def calc_eficiencia(eta0, alfa1, alfa2, Ti, Tf, G):\n", " eficiencia = eta0 - alfa1*(Tf-Ti)/G - alfa2*(Tf-Ti)**2/G\n", " return eficiencia" ] }, { "cell_type": "code", "execution_count": null, "id": "fd269462-f7ef-4004-8d5c-d13e5dbb9fc2", "metadata": {}, "outputs": [], "source": [ "df_diaria['Eficiência'] = df_diaria['Temperatura (ºC)'].apply(\n", " lambda Ti: calc_eficiencia(eta0, alfa1, alfa2, Ti, Tf=50, G=700)\n", ")\n", "df_diaria" ] }, { "cell_type": "markdown", "id": "b317fc79-e70d-4a5a-a2b8-008a8920c21f", "metadata": {}, "source": [ "Agora que temos a eficiência, podemos calcular a energia fornecida pelo coletor." ] }, { "cell_type": "code", "execution_count": null, "id": "c6c84b55-15f7-46e1-8b71-cc8073292470", "metadata": {}, "outputs": [], "source": [ "df_diaria['Energia fornecida (kWh)'] = # TODO: calcula a energia fornecida\n", "\n", "df_diaria" ] }, { "cell_type": "markdown", "id": "672e16c7-f9eb-4fc9-94b4-0fa676b7120e", "metadata": {}, "source": [ "### **3.** Considerando que o consumo médio diário de água por pessoa é 40 litros a uma temperatura de 50ºC, e considerando um coletor com uma área de 1 m$^2$/pessoa, calcula a energia térmica útil para cada dia do ano." ] }, { "cell_type": "markdown", "id": "d18c260c-e103-4ece-af27-e492005bea5d", "metadata": {}, "source": [ "O primeiro passo é calcular a energia necessária para aquecer a água a cada dia do ano." ] }, { "cell_type": "code", "execution_count": null, "id": "30e9a7b4-c5ba-4607-943b-80ec3a71ff06", "metadata": {}, "outputs": [], "source": [ "df_diaria['Energia necessária (J)'] = # TODO: calcula a energia necessária\n", "df_diaria['Energia necessária (kWh)'] = # TODO: converte em kWh\n", "\n", "df_diaria = df_diaria.drop('Energia necessária (J)', axis=1) # não precisamos da primeira coluna\n", "\n", "df_diaria" ] }, { "cell_type": "markdown", "id": "4ad861d1-814d-4497-92ea-3e3ed3dbb9b5", "metadata": {}, "source": [ "Como determinamos a energia útil agora? Temos três cenários possíveis: a energia fornecida pelo coletor pode ser maior, igual ou menor do que a energia necessária. O que acontece em cada um dos casos?" ] }, { "cell_type": "code", "execution_count": null, "id": "1067e4d2-8c36-4804-85a2-741c251636f8", "metadata": {}, "outputs": [], "source": [ "df_diaria['Energia útil (kWh)'] = # TODO: determina a energia útil\n", "df_diaria" ] }, { "cell_type": "markdown", "id": "2c6e9f73-49f2-45f6-95b7-4eb15f30e355", "metadata": {}, "source": [ "#### a) Apresenta um gráfico que ilustre a energia fornecida, necessária e útil ao longo do ano. O que observas?" ] }, { "cell_type": "code", "execution_count": null, "id": "94e37962-6c5d-4324-a50d-283bc36df470", "metadata": {}, "outputs": [], "source": [ "# definimos uma função para reutilizar mais à frente\n", "def grafico_anual_coletor(df, area=1):\n", " plt.figure(figsize=(10, 5))\n", "\n", " plt.plot(df['Energia fornecida (kWh)'], label='Energia fornecida')\n", "\n", " # TODO: introduz a energia necessária e a energia útil, bem como os títulos dos eixos\n", " \n", " plt.xlim([df_diaria.index.min(), df_diaria.index.max()])\n", " plt.ylim(bottom=0)\n", " \n", " plt.gca().xaxis.set_major_locator(mdates.MonthLocator()) # Monthly ticks\n", " plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %d')) # Format: \"Jan 01\", \"Feb 01\", etc.\n", " plt.xticks(rotation=90, ha=\"right\")\n", "\n", " plt.title(f\"Coletor de {area} m$^2$\")\n", " plt.legend()\n", " plt.grid()\n", " plt.show()\n", "\n", "grafico_anual_coletor(df_diaria)" ] }, { "cell_type": "markdown", "id": "c396fa1d-556d-4caa-8bdc-a6ee33ce7067", "metadata": {}, "source": [ "### **4.** Calcula a eficiência total do sistema para o período de um ano." ] }, { "cell_type": "code", "execution_count": null, "id": "d738ea43-e217-4c5b-a94b-ee7a72036fe2", "metadata": {}, "outputs": [], "source": [ "# TODO: calcula a eficiência anual\n", "eficiencia_anual = \n", "print(f\"Eficiência anual: {round(eficiencia_anual, 3)}\")" ] }, { "cell_type": "markdown", "id": "93661ffc-1499-4858-ade9-64068fb69042", "metadata": {}, "source": [ "### **5.** Calcula a fração solar do sistema para o período de um ano." ] }, { "cell_type": "code", "execution_count": null, "id": "ec93e692-bf3d-40da-aa1e-95840ffe63d8", "metadata": {}, "outputs": [], "source": [ "# TODO: calcula a fração solar\n", "fracao_solar = \n", "print(f\"Fração solar: {round(fracao_solar, 3)}\")" ] }, { "cell_type": "markdown", "id": "8944c7d6-45d1-4d9a-abb0-910cab7b6b2a", "metadata": {}, "source": [ "### **6.** Considerando que o preço dos coletores é de 1000€/m$^2$ e que têm um tempo de vida de 20 anos, qual é o custo da energia útil?" ] }, { "cell_type": "code", "execution_count": null, "id": "3c2b8cb4-4679-4de7-88a5-7472fb6a759b", "metadata": {}, "outputs": [], "source": [ "# TODO: calcula o custo da energia útil\n", "custo_E_util = \n", "print(f\"Custo: {round(custo_E_util, 2)} €/kWh\")" ] }, { "cell_type": "markdown", "id": "bb17025d-c550-49a2-9360-00b87910c564", "metadata": {}, "source": [ "### **7.** Repete as questões 2 a 6, desta vez considerando diferentes áreas de coletor." ] }, { "cell_type": "code", "execution_count": null, "id": "1941a25e-f1c3-44db-8c0d-490dddaa40a4", "metadata": {}, "outputs": [], "source": [ "def modelar_coletor(df_, area_coletor, preco=1000):\n", " df = df_[['Irradiação (kWh/m2)', 'Eficiência', 'Energia necessária (kWh)']].copy()\n", "\n", " df['Energia fornecida (kWh)'] = df['Eficiência'] * df['Irradiação (kWh/m2)'] * area_coletor\n", " df['Energia útil (kWh)'] = np.minimum(df['Energia fornecida (kWh)'], df['Energia necessária (kWh)'])\n", "\n", " grafico_anual_coletor(df, area=area_coletor)\n", " \n", " E_util_anual = df['Energia útil (kWh)'].sum()\n", " irradiacao_anual = df['Irradiação (kWh/m2)'].sum() * area_coletor\n", " E_necessaria_anual = df['Energia necessária (kWh)'].sum()\n", " \n", " eficiencia_anual = E_util_anual / irradiacao_anual\n", " fracao_solar = E_util_anual / E_necessaria_anual\n", " \n", " custo_E_util = preco*area_coletor/(20*E_util_anual)\n", "\n", " return eficiencia_anual, fracao_solar, custo_E_util" ] }, { "cell_type": "code", "execution_count": null, "id": "702d1dda-37cb-45da-9b93-bcf49d26cd50", "metadata": { "scrolled": true }, "outputs": [], "source": [ "results = []\n", "\n", "for area_coletor in [0.25, 0.5, 1, 1.5, 2, 3, 4, 5, 10]:\n", " eficiencia_anual, fracao_solar, custo_E_util = modelar_coletor(df_diaria, area_coletor)\n", " \n", " results.append({\n", " 'Área do coletor (m²)': area_coletor,\n", " 'Eficiência anual': eficiencia_anual,\n", " 'Fração solar': fracao_solar,\n", " 'Custo (€/kWh)': custo_E_util\n", " })\n", "\n", "df_coletores = pd.DataFrame(results)" ] }, { "cell_type": "code", "execution_count": null, "id": "aa5d4972-181b-4aed-81eb-7a9943c230c4", "metadata": {}, "outputs": [], "source": [ "df_coletores" ] }, { "cell_type": "markdown", "id": "7c41650e-d5da-457f-b6a0-bb3779bbc02f", "metadata": {}, "source": [ "#### a) Descreve o que observas para os casos extremos - a menor (0.25 m$^2$) e a maior (10 m$^2$) área consideradas." ] }, { "cell_type": "markdown", "id": "65214720-d9f5-4b86-a932-31aee88235ad", "metadata": {}, "source": [ "#### b) Como variam a eficiência e a fração solar? Porquê?" ] }, { "cell_type": "code", "execution_count": null, "id": "266cf7eb-87d8-4fe5-be1b-9f4904731f3c", "metadata": {}, "outputs": [], "source": [ "# Create the figure and axis\n", "fig, ax1 = plt.subplots(figsize=(8, 6))\n", "\n", "# Plot Fração solar on the first y-axis\n", "ax1.plot(df_coletores['Área do coletor (m²)'], df_coletores['Fração solar'], color='tab:green', marker='s', label='Fração solar')\n", "ax1.set_xlabel('Área do coletor (m²)')\n", "ax1.set_ylabel('Fração solar', color='tab:green')\n", "ax1.tick_params(axis='y', labelcolor='tab:green')\n", "ax1.set_ylim([0,1.1])\n", "\n", "# Create a second y-axis for Custo (€/kWh)\n", "ax2 = ax1.twinx()\n", "ax2.plot(df_coletores['Área do coletor (m²)'], df_coletores['Eficiência anual'], color='tab:red', marker='o', label='Custo (€/kWh)')\n", "ax2.set_ylabel('Eficiência anual', color='tab:red')\n", "ax2.tick_params(axis='y', labelcolor='tab:red')\n", "ax2.set_ylim([0,1.1])\n", "\n", "ax1.grid(True)\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "e5e6626a-edff-46ff-b359-ac387dfadc9b", "metadata": {}, "source": [ "#### c) Como varia o custo em função da fração solar? Apresenta um gráfico." ] }, { "cell_type": "code", "execution_count": null, "id": "f0d5ff5c-d522-4ad7-aed5-bd05d6333624", "metadata": {}, "outputs": [], "source": [ "# TODO: apresenta um gráfico do custo em função da fração solar. Não te esqueças dos títulos dos eixos :)" ] }, { "cell_type": "markdown", "id": "62acdd8c-2a1b-412f-9e5b-102350bde50a", "metadata": {}, "source": [ "#### d) Que valor escolherias para a área? Justifica." ] }, { "cell_type": "markdown", "id": "3882c80a-da8b-4be7-a7bd-268dff801774", "metadata": {}, "source": [ "# Bónus" ] }, { "cell_type": "markdown", "id": "1eaaeccc-f4e4-4782-a471-e8d01cb9f952", "metadata": {}, "source": [ "Para um valor extra, constrói um gráfico diferente dos anteriores que mostre algo interessante relacionado com energia solar fotovoltaica ou térmica, e explica o que ele demonstra :)" ] }, { "cell_type": "code", "execution_count": null, "id": "8f29167c-8556-4f3a-954e-d2bc80a7f021", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.4" } }, "nbformat": 4, "nbformat_minor": 5 }