{
"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": "8e22e5ac-49f8-4a28-bf7d-94fcc353c051",
"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": "382956b5-9e3d-41b2-8790-de604c61c39f",
"metadata": {},
"source": [
"### **6.** Calcula o fator de capacidade."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cb78e030-8a3c-4540-8a1b-737165740c0b",
"metadata": {},
"outputs": [],
"source": [
"fc = # TODO: calcula o fator de capacidade em percentagem\n",
"\n",
"print(f\"Fator de capacidade: {round(fc, 1)} %\")"
]
},
{
"cell_type": "markdown",
"id": "4cb2a3ce-9c90-4f79-8034-7734485f28cc",
"metadata": {},
"source": [
"### **7.** 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",
" fc = geracao_total / (potencia * 8760) * 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, fc, 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, fc, 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",
" 'Fator de capacidade': fc,\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": [
"### **8.** 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": [
"color_col = \"Azimute\"\n",
"plt.scatter(df_results[\"Potência\"], df_results[\"Autoconsumo\"], c=df_results[color_col], cmap='coolwarm', alpha=0.7)\n",
"plt.colorbar(label=color_col)\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": "7c53f219-f0ad-4db0-a1fe-d813fdd61989",
"metadata": {},
"source": [
"#### a) Porque é que a autossuficiência não tende para 100%?"
]
},
{
"cell_type": "markdown",
"id": "30929e28-3a55-4b4c-8a3b-aa8eec5c545e",
"metadata": {},
"source": [
"#### b) Qual o impacto do azimute no autoconsumo e autossuficiência? Como interpretas isso?"
]
},
{
"cell_type": "markdown",
"id": "b1b48ecd-fd10-44af-98f9-aeb362b06f9e",
"metadata": {},
"source": [
"### **9.** O que poderias fazer, a nível de comportamentos/consumos, para aumentar o teu autoconsumo e autossuficiência? Porquê?"
]
},
{
"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$."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f9ec985d-6398-4252-862e-43f1015f8760",
"metadata": {},
"outputs": [],
"source": [
"eta0 = \n",
"alfa1 = \n",
"alfa2 = \n",
"G =\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 do coletor (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 pelo coletor, 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 do coletor (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 do coletor (kWh)'] = df['Eficiência'] * df['Irradiação (kWh/m2)'] * area_coletor\n",
" df['Energia útil (kWh)'] = np.minimum(df['Energia do coletor (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. Compara os gráficos, a eficiência, a fração solar e o custo."
]
},
{
"cell_type": "markdown",
"id": "65214720-d9f5-4b86-a932-31aee88235ad",
"metadata": {},
"source": [
"#### b) Como variam a eficiência e a fração solar em função da área do coletor? 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 até dois valores extra, constrói um gráfico diferente dos anteriores que mostre algo interessante relacionado com recurso solar, e explica o que ele demonstra :)\n",
"\n",
"Utiliza os valores calculados nas questões anteriores, ou faz os teus próprios cálculos --- não uses valores arbitrários."
]
},
{
"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
}