From 3ff5c23d63a4f0d8f9a1385fea622f33d2ff2e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Let=C3=ADciaSDV?= <136032715+leticiasdrummond@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:02:33 -0300 Subject: [PATCH] =?UTF-8?q?Estrutura=20projeto=20para=20reprodutibilidade?= =?UTF-8?q?=20de=20modelos=20energ=C3=A9ticos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + ENVIRONMENT.md | 25 ++ Makefile | 9 + README.md | 66 +++- assumptions.md | 28 ++ changelog.md | 19 + configs/caso_base.yaml | 17 + configs/caso_modificado.yaml | 17 + data/interim/.gitkeep | 0 data/processed/.gitkeep | 0 data/raw/.gitkeep | 0 data_dictionary.md | 38 ++ docs/.gitkeep | 0 ...BASE_Modificado_ELETROPOSTO_11_02_26.ipynb | 0 .../1_CASO_BASE_ELETROPOSTO_11_02_26.ipynb | 0 requirements.txt | 5 + results/.gitkeep | 0 run_all.sh | 7 + scripts/.gitkeep | 0 scripts/extract_notebook_code.py | 31 ++ src/__init__.py | 1 + src/model.py | 49 +++ ...SO_BASE_Modificado_ELETROPOSTO_11_02_26.py | 329 ++++++++++++++++++ .../1_CASO_BASE_ELETROPOSTO_11_02_26.py | 253 ++++++++++++++ src/run_scenario.py | 75 ++++ 25 files changed, 970 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 ENVIRONMENT.md create mode 100644 Makefile create mode 100644 assumptions.md create mode 100644 changelog.md create mode 100644 configs/caso_base.yaml create mode 100644 configs/caso_modificado.yaml create mode 100644 data/interim/.gitkeep create mode 100644 data/processed/.gitkeep create mode 100644 data/raw/.gitkeep create mode 100644 data_dictionary.md create mode 100644 docs/.gitkeep rename 1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.ipynb => notebooks/1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.ipynb (100%) rename 1_CASO_BASE_ELETROPOSTO_11_02_26.ipynb => notebooks/1_CASO_BASE_ELETROPOSTO_11_02_26.ipynb (100%) create mode 100644 requirements.txt create mode 100644 results/.gitkeep create mode 100755 run_all.sh create mode 100644 scripts/.gitkeep create mode 100644 scripts/extract_notebook_code.py create mode 100644 src/__init__.py create mode 100644 src/model.py create mode 100644 src/notebook_exports/1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.py create mode 100644 src/notebook_exports/1_CASO_BASE_ELETROPOSTO_11_02_26.py create mode 100644 src/run_scenario.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md new file mode 100644 index 0000000..8e09d89 --- /dev/null +++ b/ENVIRONMENT.md @@ -0,0 +1,25 @@ +# Environment Metadata + +## Sistema +- SO: Linux (container) +- Shell: bash +- Python: 3.10.19 + +## Solver e modelagem +- Modelagem: Pyomo +- Solver principal: CBC +- Solvers alternativos: HiGHS / Gurobi (quando disponíveis) + +## Reprodutibilidade +- Dependências listadas em `requirements.txt` +- Cenários parametrizados em `configs/*.yaml` +- Execução padrão por `src/run_scenario.py` + +## Coleta automática recomendada +Para registrar snapshots do ambiente, execute e anexe o resultado em `docs/`: + +```bash +python --version +pip freeze > docs/pip_freeze.txt +cbc -stop +``` diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..65ba30c --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: run-base run-mod run-all + +run-base: + python src/run_scenario.py --config configs/caso_base.yaml --output results/caso_base + +run-mod: + python src/run_scenario.py --config configs/caso_modificado.yaml --output results/caso_modificado + +run-all: run-base run-mod diff --git a/README.md b/README.md index c7e9d40..2f58e2c 100644 --- a/README.md +++ b/README.md @@ -1 +1,65 @@ -# Modelos-Base \ No newline at end of file +# Otimização de BESS para Microgrid Comercial com FV e Eletroposto + +Repositório para desenvolvimento, análise e reprodução de modelos de otimização energética (MILP) para um pequeno comércio com geração fotovoltaica (FV), sistema de armazenamento em bateria (BESS) e carga de eletroposto. + +## Objetivo + +Minimizar o custo operacional diário de energia, respeitando: +- balanço de energia por hora, +- dinâmica de estado de carga (SOC) da bateria, +- limites operacionais da bateria, +- compra e exportação de energia para a rede. + +## Escopo atual + +- Modelos em notebooks Jupyter (caso base e caso modificado) +- Estrutura reprodutível com scripts Python em `src/` +- Configuração de cenários em YAML (`configs/`) +- Documentação de hipóteses, dados e ambiente + +## Estrutura de pastas + +```text +Modelos-Base/ +├── notebooks/ # Notebooks originais +├── src/ # Código modular executável +├── configs/ # Cenários de entrada (.yaml) +├── data/ +│ ├── raw/ # Dados brutos +│ ├── interim/ # Dados intermediários +│ └── processed/ # Dados tratados +├── results/ # Saídas (csv, figuras, logs) +├── docs/ # Documentação complementar +├── scripts/ # Scripts utilitários/automação +├── assumptions.md +├── changelog.md +├── data_dictionary.md +├── ENVIRONMENT.md +└── requirements.txt +``` + +## Execução rápida + +1. Crie e ative ambiente virtual. +2. Instale dependências: + +```bash +pip install -r requirements.txt +``` + +3. Rode um cenário: + +```bash +python src/run_scenario.py --config configs/caso_base.yaml --output results/caso_base +``` + +## Versionamento científico + +- Mudanças de modelagem: `changelog.md` +- Assunções e limitações: `assumptions.md` +- Dicionário de dados: `data_dictionary.md` +- Metadados de ambiente: `ENVIRONMENT.md` + +## Versão + +Versão inicial proposta: **v0.1** (caso base com eficiência da bateria). diff --git a/assumptions.md b/assumptions.md new file mode 100644 index 0000000..c1f7db4 --- /dev/null +++ b/assumptions.md @@ -0,0 +1,28 @@ +# Assumptions (Assunções de Modelagem) + +## Escopo temporal e granularidade +- **A1**: Horizonte de otimização de 24 horas com passo de 1 hora. +- **A2**: Todos os perfis de demanda, geração FV e tarifa são determinísticos no horizonte diário. + +## Sistema elétrico +- **A3**: O sistema pode importar energia da rede em qualquer hora, limitado apenas por restrições do modelo. +- **A4**: Exportação de energia para rede é permitida e remunerada por tarifa definida no cenário. +- **A5**: Perdas na rede interna do comércio não são modeladas explicitamente. + +## BESS +- **A6**: A bateria possui limites fixos de potência de carga/descarga e capacidade energética. +- **A7**: Eficiência de carga/descarga é tratada de forma agregada por parâmetro de eficiência. +- **A8**: SOC inicial e final são definidos por cenário e devem ser fisicamente factíveis. +- **A9**: Degradação da bateria não é modelada explicitamente no objetivo (sem custo de ciclo nesta versão). + +## Eletroposto e cargas +- **A10**: A demanda do eletroposto é considerada exógena e deve ser plenamente atendida. +- **A11**: Não há flexibilidade temporal de carga (sem deslocamento de demanda) nesta versão. + +## Formulação e solução +- **A12**: Modelo MILP resolvido com CBC (alternativamente HiGHS/Gurobi, quando disponível). +- **A13**: Solução ótima depende da consistência das unidades e dos limites de parâmetros. + +## Visualização e análise +- **A14**: Gráficos e tabelas são pós-processamento e não alteram a solução do otimizador. +- **A15**: Resultados são válidos apenas para os cenários de entrada definidos em `configs/`. diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..3da6814 --- /dev/null +++ b/changelog.md @@ -0,0 +1,19 @@ +# Changelog + +Este arquivo registra mudanças relevantes de estrutura, modelagem, dados e reprodutibilidade. + +## [v0.1.0] - 2026-02-13 + +### Added +- Estrutura profissional de pastas (`notebooks`, `src`, `configs`, `data/*`, `results`, `docs`, `scripts`). +- Documentação de base: `README.md`, `assumptions.md`, `data_dictionary.md`, `ENVIRONMENT.md`. +- Cenários YAML iniciais para caso base e caso modificado. +- Script executável standalone para reproduzir cenário (`src/run_scenario.py`). +- Script de execução em lote (`run_all.sh`). +- Dependências fixadas em `requirements.txt`. + +### Changed +- Notebooks originais movidos para `notebooks/` para separar prototipação de código de produção. + +### Notes +- Próxima etapa: extração incremental de funções dos notebooks para módulos menores (`src/model/`, `src/io/`, `src/plots/`). diff --git a/configs/caso_base.yaml b/configs/caso_base.yaml new file mode 100644 index 0000000..0a4190e --- /dev/null +++ b/configs/caso_base.yaml @@ -0,0 +1,17 @@ +name: caso_base +horizonte_horas: 24 + +demanda_comercio: [6,6,6,6,7,8,10,12,14,15,16,17,18,18,17,16,15,14,12,10,9,8,7,6] +demanda_ev: [0,0,0,0,0,0,5,8,10,6,4,2,0,0,0,2,4,6,8,6,4,2,0,0] +geracao_pv: [0,0,0,0,0,1,2,4,6,8,10,12,11,9,7,5,3,1,0,0,0,0,0,0] + +tarifa_compra: [0.15,0.20,0.25,0.30,0.35,0.40,0.45,0.50,0.45,0.40,0.35,0.30,0.25,0.20,0.15,0.10,0.10,0.15,0.20,0.25,0.30,0.35,0.40,0.45] +tarifa_venda: [0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.10,0.10,0.10,0.10,0.10,0.10,0.10,0.10,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08] + +bess_capacidade: 20.0 +bess_pot_carga_max: 5.0 +bess_pot_descarga_max: 5.0 +bess_soc_inicial: 10.0 +bess_soc_min: 2.0 +bess_soc_max: 20.0 +bess_eficiencia: 0.95 diff --git a/configs/caso_modificado.yaml b/configs/caso_modificado.yaml new file mode 100644 index 0000000..952a52c --- /dev/null +++ b/configs/caso_modificado.yaml @@ -0,0 +1,17 @@ +name: caso_modificado +horizonte_horas: 24 + +demanda_comercio: [6,6,6,6,7,8,10,12,14,15,16,17,18,18,17,16,15,14,12,10,9,8,7,6] +demanda_ev: [0,0,0,0,0,0,5,8,10,6,4,2,0,0,0,2,4,6,8,6,4,2,0,0] +geracao_pv: [0,0,0,0,0,1,2,4,6,8,11,13,12,10,8,6,4,2,0,0,0,0,0,0] + +tarifa_compra: [0.15,0.20,0.25,0.30,0.35,0.40,0.45,0.50,0.45,0.40,0.35,0.30,0.25,0.20,0.15,0.10,0.10,0.15,0.20,0.25,0.30,0.35,0.40,0.45] +tarifa_venda: [0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.10,0.10,0.10,0.10,0.10,0.10,0.10,0.10,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08,0.08] + +bess_capacidade: 24.0 +bess_pot_carga_max: 6.0 +bess_pot_descarga_max: 6.0 +bess_soc_inicial: 12.0 +bess_soc_min: 2.0 +bess_soc_max: 24.0 +bess_eficiencia: 0.93 diff --git a/data/interim/.gitkeep b/data/interim/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/processed/.gitkeep b/data/processed/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/raw/.gitkeep b/data/raw/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data_dictionary.md b/data_dictionary.md new file mode 100644 index 0000000..104dc88 --- /dev/null +++ b/data_dictionary.md @@ -0,0 +1,38 @@ +# Data Dictionary + +## Entidades principais + +| Campo | Tipo | Unidade | Descrição | Origem | +|---|---|---:|---|---| +| `horizonte_horas` | int | h | Número de períodos de otimização | Configuração de cenário | +| `demanda_comercio` | list[float] | kWh/h | Demanda horária do comércio | Entrada do cenário | +| `demanda_ev` | list[float] | kWh/h | Demanda horária do eletroposto | Entrada do cenário | +| `geracao_pv` | list[float] | kWh/h | Geração FV horária | Entrada do cenário | +| `tarifa_compra` | list[float] | R$/kWh | Tarifa de compra da rede por hora | Entrada do cenário | +| `tarifa_venda` | list[float] | R$/kWh | Tarifa de exportação por hora | Entrada do cenário | +| `bess_capacidade` | float | kWh | Capacidade nominal da bateria | Entrada do cenário | +| `bess_pot_carga_max` | float | kW | Limite de potência de carga | Entrada do cenário | +| `bess_pot_descarga_max` | float | kW | Limite de potência de descarga | Entrada do cenário | +| `bess_soc_inicial` | float | kWh | SOC no período inicial | Entrada do cenário | +| `bess_soc_min` | float | kWh | Limite mínimo de SOC | Entrada do cenário | +| `bess_soc_max` | float | kWh | Limite máximo de SOC | Entrada do cenário | +| `bess_eficiencia` | float | adim. | Eficiência (0-1) aplicada ao armazenamento | Entrada do cenário | + +## Variáveis de decisão (saída do modelo) + +| Campo | Tipo | Unidade | Descrição | +|---|---|---:|---| +| `grid_import[t]` | float | kWh/h | Energia importada da rede em `t` | +| `grid_export[t]` | float | kWh/h | Energia exportada para rede em `t` | +| `bess_charge[t]` | float | kWh/h | Carga da bateria em `t` | +| `bess_discharge[t]` | float | kWh/h | Descarga da bateria em `t` | +| `soc[t]` | float | kWh | Estado de carga da bateria em `t` | +| `objective_value` | float | R$ | Custo total minimizado no horizonte | + +## Regras de validação recomendadas + +1. Comprimento das séries horárias igual a `horizonte_horas`. +2. Valores não negativos para demanda e geração. +3. `0 < bess_eficiencia <= 1`. +4. `bess_soc_min <= bess_soc_inicial <= bess_soc_max`. +5. `bess_soc_max <= bess_capacidade`. diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.ipynb b/notebooks/1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.ipynb similarity index 100% rename from 1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.ipynb rename to notebooks/1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.ipynb diff --git a/1_CASO_BASE_ELETROPOSTO_11_02_26.ipynb b/notebooks/1_CASO_BASE_ELETROPOSTO_11_02_26.ipynb similarity index 100% rename from 1_CASO_BASE_ELETROPOSTO_11_02_26.ipynb rename to notebooks/1_CASO_BASE_ELETROPOSTO_11_02_26.ipynb diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..22e70e1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pyomo==6.8.2 +pandas==2.2.2 +numpy==1.26.4 +matplotlib==3.9.0 +PyYAML==6.0.2 diff --git a/results/.gitkeep b/results/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/run_all.sh b/run_all.sh new file mode 100755 index 0000000..593ddb8 --- /dev/null +++ b/run_all.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +python src/run_scenario.py --config configs/caso_base.yaml --output results/caso_base +python src/run_scenario.py --config configs/caso_modificado.yaml --output results/caso_modificado + +echo "Execução completa. Veja a pasta results/." diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/extract_notebook_code.py b/scripts/extract_notebook_code.py new file mode 100644 index 0000000..5c087c1 --- /dev/null +++ b/scripts/extract_notebook_code.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +NOTEBOOKS = ROOT / "notebooks" +OUT = ROOT / "src" / "notebook_exports" +OUT.mkdir(parents=True, exist_ok=True) + +for nb_path in NOTEBOOKS.glob("*.ipynb"): + data = json.loads(nb_path.read_text(encoding="utf-8")) + lines = [ + '"""Arquivo extraído automaticamente do notebook para rastreabilidade.\n', + "NÃO editável manualmente sem sincronização com o notebook de origem.\n", + '"""\n\n', + ] + for cell in data.get("cells", []): + if cell.get("cell_type") != "code": + continue + src = "".join(cell.get("source", [])) + if src.lstrip().startswith("!") or "MEU_TOKEN" in src or "gurobipy" in src: + continue + lines.append("\n# %%\n") + lines.append(src) + if not src.endswith("\n"): + lines.append("\n") + + out_path = OUT / f"{nb_path.stem}.py" + out_path.write_text("".join(lines), encoding="utf-8") + print(f"Gerado: {out_path.relative_to(ROOT)}") diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..4e54c42 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +"""Pacote de otimização energética (MILP) para microgrid comercial com FV, BESS e eletroposto.""" diff --git a/src/model.py b/src/model.py new file mode 100644 index 0000000..4eb059d --- /dev/null +++ b/src/model.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import pyomo.environ as pyo + + +def build_model(cfg: dict) -> pyo.ConcreteModel: + n = int(cfg["horizonte_horas"]) + T = range(n) + + m = pyo.ConcreteModel() + m.T = pyo.Set(initialize=T) + + demanda_total = [c + e for c, e in zip(cfg["demanda_comercio"], cfg["demanda_ev"])] + pv = cfg["geracao_pv"] + buy = cfg["tarifa_compra"] + sell = cfg["tarifa_venda"] + + eta = float(cfg["bess_eficiencia"]) + + m.P_grid = pyo.Var(m.T, domain=pyo.NonNegativeReals) + m.P_export = pyo.Var(m.T, domain=pyo.NonNegativeReals) + m.P_charge = pyo.Var(m.T, domain=pyo.NonNegativeReals) + m.P_discharge = pyo.Var(m.T, domain=pyo.NonNegativeReals) + m.SOC = pyo.Var(m.T, bounds=(cfg["bess_soc_min"], cfg["bess_soc_max"])) + + m.obj = pyo.Objective( + expr=sum(buy[t] * m.P_grid[t] - sell[t] * m.P_export[t] for t in m.T), + sense=pyo.minimize, + ) + + def balanco_regra(mm, t): + return ( + mm.P_grid[t] + pv[t] + mm.P_discharge[t] + == demanda_total[t] + mm.P_charge[t] + mm.P_export[t] + ) + + m.balanco = pyo.Constraint(m.T, rule=balanco_regra) + + def soc_regra(mm, t): + if t == 0: + return mm.SOC[t] == cfg["bess_soc_inicial"] + eta * mm.P_charge[t] - (1 / eta) * mm.P_discharge[t] + return mm.SOC[t] == mm.SOC[t - 1] + eta * mm.P_charge[t] - (1 / eta) * mm.P_discharge[t] + + m.soc_dinamica = pyo.Constraint(m.T, rule=soc_regra) + + m.charge_limit = pyo.Constraint(m.T, rule=lambda mm, t: mm.P_charge[t] <= cfg["bess_pot_carga_max"]) + m.discharge_limit = pyo.Constraint(m.T, rule=lambda mm, t: mm.P_discharge[t] <= cfg["bess_pot_descarga_max"]) + + return m diff --git a/src/notebook_exports/1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.py b/src/notebook_exports/1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.py new file mode 100644 index 0000000..f90eee8 --- /dev/null +++ b/src/notebook_exports/1_2_CASO_BASE_Modificado_ELETROPOSTO_11_02_26.py @@ -0,0 +1,329 @@ +"""Arquivo extraído automaticamente do notebook para rastreabilidade. +NÃO editável manualmente sem sincronização com o notebook de origem. +""" + + +# %% + +# ========================================================= + +T = range(24) # horizonte horário +''' +# ========================================================= +# Caso Base - 1 : Verificado com o Gui em 11/02/2026 +# ========================================================= + +# Demanda do comércio (kWh) +demanda_comercio = [ + 6, 6, 6, 6, 7, 8, 10, 12, 14, 15, 16, 17, + 18, 18, 17, 16, 15, 14, 12, 10, 9, 8, 7, 6 +] + +# Demanda do eletroposto (kWh) +demanda_ev = [ + 0, 0, 0, 0, 0, 0, + 5, 8, 10, 6, 4, 2, + 0, 0, 0, 2, 4, 6, + 8, 6, 4, 2, 0, 0 +] + +# Geração fotovoltaica (kWh) +geracao_pv = [ + 0, 0, 0, 0, 0, 0, + 2, 5, 8, 12, 15, 18, + 20, 18, 15, 10, 6, 3, + 1, 0, 0, 0, 0, 0 +] +''' +# ============================================================ +# DADOS DE ENTRADA CORRIGIDOS (24 HORAS) +# ============================================================ + +# Demanda do comércio (kW) +# Ajustado para que a soma seja 720 (Média exata = 30 kW) +demanda_comercio = [ + 5, 5, 5, 5, 5, 8, # 00h - 05h (Madrugada baixa) + 12, 25, 40, 50, 60, 65, # 06h - 11h (Subida manhã) + 65, 60, 55, 50, 45, 40, # 12h - 17h (Tarde alta) + 35, 30, 25, 15, 10, 5 # 18h - 23h (Descida noite) +] + +# Demanda do eletroposto (kW) +# Considera 2 carregadores de 50kW (Max 100kW) com dois picos de uso +demanda_ev = [ + 0, 0, 0, 0, 0, 0, # 00h - 05h + 10, 40, 80, 95, 70, 40, # 06h - 11h (Pico Manhã ~09h) + 20, 20, 30, 50, 80, 100, # 12h - 17h (Carga leve tarde -> Início Pico) + 90, 60, 30, 10, 0, 0 # 18h - 23h (Pico Noite ~18h e fim) +] + +# Geração fotovoltaica (kW) +# Compatível com sistema de 50 kWp (Pico ao meio-dia) +geracao_pv = [ + 0, 0, 0, 0, 0, 0, # 00h - 05h (Sem sol) + 2, 12, 28, 42, 48, 50, # 06h - 11h (Amanhecer até pico) + 50, 48, 42, 28, 12, 2, # 12h - 17h (Pico até anoitecer) + 0, 0, 0, 0, 0, 0 # 18h - 23h (Sem sol) +] + +# Custos (R$/kWh) +custo_compra = 0.75 +preco_venda = 0.40 + +# Parâmetros da bateria +capacidade_bess = 50.0 # kWh +potencia_max_bess = 15.0 # kW +soc_min = 0.20 * capacidade_bess +soc_max = 0.95 * capacidade_bess +soc_inicial = 0.50 * capacidade_bess + + +eta_c = 0.955 # eficiência de carga +eta_d = 0.955 # eficiência de descarga + +# Custo de degradação (R$/kWh throughput) +custo_degradacao = 0.08 + +# %% +# ========================================================= +# 2. CRIAÇÃO DO MODELO +# ========================================================= + +model = pyo.ConcreteModel() + +model.T = pyo.Set(initialize=T) + +# ========================================================= +# 3. VARIÁVEIS DE DECISÃO +# ========================================================= + +model.P_grid = pyo.Var(model.T, domain=pyo.NonNegativeReals) # compra da rede +model.P_export = pyo.Var(model.T, domain=pyo.NonNegativeReals) # venda à rede + +model.P_charge = pyo.Var(model.T, domain=pyo.NonNegativeReals) # carga da bateria +model.P_discharge = pyo.Var(model.T, domain=pyo.NonNegativeReals) # descarga da bateria + +model.SOC = pyo.Var(model.T, domain=pyo.NonNegativeReals) + +# Variáveis binárias (bloqueio simultâneo) +model.u_charge = pyo.Var(model.T, domain=pyo.Binary) +model.u_discharge = pyo.Var(model.T, domain=pyo.Binary) + +# ========================================================= +# 4. FUNÇÃO OBJETIVO +# ========================================================= + +def objective_rule(m): + custo_energia = sum(custo_compra * m.P_grid[t] for t in m.T) + receita_venda = sum(preco_venda * m.P_export[t] for t in m.T) + custo_deg = sum( + custo_degradacao * (m.P_charge[t] + m.P_discharge[t]) + for t in m.T + ) + return custo_energia + custo_deg - receita_venda + +model.OBJ = pyo.Objective(rule=objective_rule, sense=pyo.minimize) + +# ========================================================= +# 5. RESTRIÇÕES +# ========================================================= + +# 5.1 Balanço de energia +def energy_balance_rule(m, t): + demanda_total = demanda_comercio[t] + demanda_ev[t] + return ( + m.P_grid[t] + + geracao_pv[t] + + m.P_discharge[t] + == + demanda_total + + m.P_charge[t] + + m.P_export[t] + ) + +model.energy_balance = pyo.Constraint(model.T, rule=energy_balance_rule) + + +# Implementação da Restrição 4.2: Dinâmica do SOC, com perdas explicitadas +def soc_rule(m, t): + # Losses associated with charging + charge_losses_kW = (1 - eta_c) * m.P_charge[t] + # Losses associated with discharging + # Power drawn from storage is P_discharge / eta_d + # So discharge losses are (P_discharge / eta_d) - P_discharge + discharge_losses_kW = (1/eta_d - 1) * m.P_discharge[t] + + # Net change in SOC = (Energy_Charged_Gross - Charge_Losses) - (Energy_Discharged_Gross + Discharge_Losses) + # This is equivalent to: (eta_c * P_charge) - (P_discharge / eta_d) + + if t == 0: + # SOC[t] = soc_inicial + RECARGA_BRUTA - PERDAS_CARGA - (DESCARGA_BRUTA + PERDAS_DESCARGA) + return m.SOC[t] == soc_inicial + (m.P_charge[t] - charge_losses_kW) - (m.P_discharge[t] + discharge_losses_kW) + + # SOC[t] = SOC[t-1] + RECARGA_BRUTA - PERDAS_CARGA - (DESCARGA_BRUTA + PERDAS_DESCARGA) + return m.SOC[t] == m.SOC[t-1] + (m.P_charge[t] - charge_losses_kW) - (m.P_discharge[t] + discharge_losses_kW) + +# Reatribuindo (ou criando, se não existisse) a restrição soc_dyn no modelo +# O Pyomo pode emitir um WARNING se a restrição já existir, indicando que ela está sendo substituída. +model.soc_dyn = pyo.Constraint(model.T, rule=soc_rule) +# 5.3 Limites de SOC +model.soc_min = pyo.Constraint(model.T, rule=lambda m, t: m.SOC[t] >= soc_min) +model.soc_max = pyo.Constraint(model.T, rule=lambda m, t: m.SOC[t] <= soc_max) +model.soc_amanha = pyo.Constraint(expr= model.SOC[23] >= soc_inicial) #incluido junto do Guilherme para garantir que o BESS esteja minimamente carregado no final das 24h +# 5.4 Limites de potência com binárias +model.charge_limit = pyo.Constraint( + model.T, rule=lambda m, t: m.P_charge[t] <= potencia_max_bess * m.u_charge[t] +) + +model.discharge_limit = pyo.Constraint( + model.T, rule=lambda m, t: m.P_discharge[t] <= potencia_max_bess * m.u_discharge[t] +) + +# 5.5 Bloqueio de carga e descarga simultâneas +model.no_simultaneous = pyo.Constraint( + model.T, rule=lambda m, t: m.u_charge[t] + m.u_discharge[t] <= 1 +) + + + +# ========================================================= +# 6. RESOLUÇÃO +# ========================================================= + +solver = pyo.SolverFactory("cbc") +results = solver.solve(model, tee=False) + +# ========================================================= +# 7. RESULTADOS +# ========================================================= + +# Define column names explicitly to avoid hidden character issues +col_demanda_comercio = "Demanda_Comercio".strip() +col_demanda_ev = "Demanda_EV".strip() +col_pv = "PV".strip() +col_grid = "Grid".strip() +col_export = "Export".strip() +col_carga_bess = "Carga_BESS".strip() +col_descarga_bess = "Descarga_BESS".strip() +col_soc = "SOC".strip() +col_hora = "Hora".strip() + +df = pd.DataFrame({ + col_hora: list(T), + col_demanda_comercio: demanda_comercio, + col_demanda_ev: demanda_ev, + col_pv: geracao_pv, + col_grid: [pyo.value(model.P_grid[t]) for t in T], + col_export: [pyo.value(model.P_export[t]) for t in T], + col_carga_bess: [pyo.value(model.P_charge[t]) for t in T], + col_descarga_bess: [pyo.value(model.P_discharge[t]) for t in T], + col_soc: [pyo.value(model.SOC[t]) for t in T] +}) + +print(df) +print("Custo total (R$):", pyo.value(model.OBJ)) + +print(f"Valor da Função Objetivo (Custo Total): R$ {pyo.value(model.OBJ):.2f}") + +# %% + +# ========================================================= +# 8. GRÁFICOS +# ========================================================= + +plt.figure() +plt.plot(df[col_hora], df[col_soc]) +plt.xlabel("Hora") +plt.ylabel("SOC (kWh)") +plt.title("Estado de Carga da Bateria") +plt.grid() +plt.show() + +# %% +# Define column names explicitly to avoid hidden character issues +col_demanda_comercio = "Demanda_Comercio".strip() +col_demanda_ev = "Demanda_EV".strip() +col_pv = "PV".strip() +col_grid = "Grid".strip() +col_export = "Export".strip() +col_carga_bess = "Carga_BESS".strip() +col_descarga_bess = "Descarga_BESS".strip() +col_soc = "SOC".strip() +col_hora = "Hora".strip() + +plt.figure(figsize=(14, 8)) + +# Plotando a demanda total +df['Demanda_Total'] = df[col_demanda_comercio] + df[col_demanda_ev] +plt.plot(df[col_hora], df['Demanda_Total'], label='Demanda Total', color='red', linestyle='--', linewidth=2) + +# Plotando as fontes de energia +plt.plot(df[col_hora], df[col_pv], label='Geração Fotovoltaica (PV)', color='green', marker='o', markersize=4) +plt.plot(df[col_hora], df[col_grid], label='Compra da Rede', color='blue', marker='x', markersize=4) +plt.plot(df[col_hora], df[col_descarga_bess], label='Descarga da Bateria', color='purple', marker='^', markersize=4) +plt.plot(df[col_hora], df[col_carga_bess], label='Carga da Bateria', color='orange', marker='v', markersize=4) +plt.plot(df[col_hora], df[col_export], label='Venda para a Rede', color='cyan', marker='s', markersize=4) + +plt.xlabel('Hora do Dia') +plt.ylabel('Potência (kW)') +plt.title('Fluxo de Carga do Sistema de Energia') +plt.legend() +plt.grid(True, linestyle=':', alpha=0.7) +plt.xticks(df[col_hora]) +plt.tight_layout() +plt.show() + +# %% +import matplotlib.pyplot as plt +import pandas as pd +from pyomo.environ import * + +# Definição do modelo +model = ConcreteModel() +model.T = RangeSet(0, 23) # 24 horas + +# Parâmetros +battery_power_max = 5 # potência máxima da bateria (kW) +price_tou = {t: preco for t, preco in enumerate([0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.45, 0.40, 0.35, 0.30, 0.25, 0.20, 0.15, 0.10, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45])} + +# Variáveis +model.E_ch = Var(model.T, domain=NonNegativeReals) # energia carregada +model.E_dis = Var(model.T, domain=NonNegativeReals) # energia descarregada + +# Variáveis binárias para impedir carga e descarga simultâneas +model.y_ch = Var(model.T, domain=Binary) +model.y_dis = Var(model.T, domain=Binary) + +# Restrições para ligar variáveis contínuas às binárias +model.ChargeLogic = Constraint(model.T, rule=lambda m, t: m.E_ch[t] <= battery_power_max * m.y_ch[t]) +model.DischargeLogic = Constraint(model.T, rule=lambda m, t: m.E_dis[t] <= battery_power_max * m.y_dis[t]) +model.ExclusiveCharge = Constraint(model.T, rule=lambda m, t: m.y_ch[t] + m.y_dis[t] <= 1) + +# Função objetivo: minimizar custo total considerando TOU +model.obj = Objective(expr=sum(price_tou[t] * model.E_ch[t] for t in model.T), sense=minimize) + +# Solver +solver = SolverFactory('glpk') +result = solver.solve(model) + +# Extrair resultados +hours = list(model.T) +charge = [value(model.E_ch[t]) for t in model.T] +discharge = [value(model.E_dis[t]) for t in model.T] + +# Criar DataFrame para visualização +df = pd.DataFrame({'Hora': hours, 'Carga (kW)': charge, 'Descarga (kW)': discharge, 'Preço TOU ($/kWh)': [price_tou[t] for t in hours]}) + +print(df) + +# Gráficos +plt.figure(figsize=(12,6)) +plt.plot(df['Hora'], df['Carga (kW)'], label='Carga (kW)', marker='o') +plt.plot(df['Hora'], df['Descarga (kW)'], label='Descarga (kW)', marker='x') +plt.bar(df['Hora'], df['Preço TOU ($/kWh)'], alpha=0.3, label='Preço TOU ($/kWh)') +plt.xlabel('Hora do Dia') +plt.ylabel('Potência / Preço') +plt.title('Carga, Descarga e Preço TOU ao longo do dia') +plt.legend() +plt.grid(True) +plt.show() diff --git a/src/notebook_exports/1_CASO_BASE_ELETROPOSTO_11_02_26.py b/src/notebook_exports/1_CASO_BASE_ELETROPOSTO_11_02_26.py new file mode 100644 index 0000000..dda1e20 --- /dev/null +++ b/src/notebook_exports/1_CASO_BASE_ELETROPOSTO_11_02_26.py @@ -0,0 +1,253 @@ +"""Arquivo extraído automaticamente do notebook para rastreabilidade. +NÃO editável manualmente sem sincronização com o notebook de origem. +""" + + +# %% +# ========================================================= +# 1. DADOS DE ENTRADA (EXEMPLO FACTÍVEL) +# ========================================================= + +T = range(24) # horizonte horário + +# Demanda do comércio (kWh) +demanda_comercio = [ + 6, 6, 6, 6, 7, 8, 10, 12, 14, 15, 16, 17, + 18, 18, 17, 16, 15, 14, 12, 10, 9, 8, 7, 6 +] + +# Demanda do eletroposto (kWh) +demanda_ev = [ + 0, 0, 0, 0, 0, 0, + 5, 8, 10, 6, 4, 2, + 0, 0, 0, 2, 4, 6, + 8, 6, 4, 2, 0, 0 +] + +# Geração fotovoltaica (kWh) +geracao_pv = [ + 0, 0, 0, 0, 0, 0, + 2, 5, 8, 12, 15, 18, + 20, 18, 15, 10, 6, 3, + 1, 0, 0, 0, 0, 0 +] + +# Custos (R$/kWh) +custo_compra = 0.75 +preco_venda = 0.40 + +# Parâmetros da bateria +capacidade_bess = 50.0 # kWh +potencia_max_bess = 15.0 # kW +soc_min = 0.20 * capacidade_bess +soc_max = 0.95 * capacidade_bess +soc_inicial = 0.50 * capacidade_bess + + +eta_c = 0.955 # eficiência de carga +eta_d = 0.955 # eficiência de descarga + +# Custo de degradação (R$/kWh throughput) +custo_degradacao = 0.08 + +# %% +# ========================================================= +# 2. CRIAÇÃO DO MODELO +# ========================================================= + +model = pyo.ConcreteModel() + +model.T = pyo.Set(initialize=T) + +# ========================================================= +# 3. VARIÁVEIS DE DECISÃO +# ========================================================= + +model.P_grid = pyo.Var(model.T, domain=pyo.NonNegativeReals) # compra da rede +model.P_export = pyo.Var(model.T, domain=pyo.NonNegativeReals) # venda à rede + +model.P_charge = pyo.Var(model.T, domain=pyo.NonNegativeReals) # carga da bateria +model.P_discharge = pyo.Var(model.T, domain=pyo.NonNegativeReals) # descarga da bateria + +model.SOC = pyo.Var(model.T, domain=pyo.NonNegativeReals) + +# Variáveis binárias (bloqueio simultâneo) +model.u_charge = pyo.Var(model.T, domain=pyo.Binary) +model.u_discharge = pyo.Var(model.T, domain=pyo.Binary) + +# ========================================================= +# 4. FUNÇÃO OBJETIVO +# ========================================================= + +def objective_rule(m): + custo_energia = sum(custo_compra * m.P_grid[t] for t in m.T) + receita_venda = sum(preco_venda * m.P_export[t] for t in m.T) + custo_deg = sum( + custo_degradacao * (m.P_charge[t] + m.P_discharge[t]) + for t in m.T + ) + return custo_energia + custo_deg - receita_venda + +model.OBJ = pyo.Objective(rule=objective_rule, sense=pyo.minimize) + +# ========================================================= +# 5. RESTRIÇÕES +# ========================================================= + +# 5.1 Balanço de energia +def energy_balance_rule(m, t): + demanda_total = demanda_comercio[t] + demanda_ev[t] + return ( + m.P_grid[t] + + geracao_pv[t] + + m.P_discharge[t] + == + demanda_total + + m.P_charge[t] + + m.P_export[t] + ) + +model.energy_balance = pyo.Constraint(model.T, rule=energy_balance_rule) +''' +# 5.2 Dinâmica do SOC +def soc_rule(m, t): + if t == 0: + return m.SOC[t] == soc_inicial + eta_c * m.P_charge[t] - (1/eta_d) * m.P_discharge[t] + return m.SOC[t] == m.SOC[t-1] + eta_c * m.P_charge[t] - (1/eta_d) * m.P_discharge[t] + +model.soc_dyn = pyo.Constraint(model.T, rule=soc_rule) +''' + +# Implementação da Restrição 4.2: Dinâmica do SOC, com perdas explicitadas +def soc_rule(m, t): + # Losses associated with charging + charge_losses_kW = (1 - eta_c) * m.P_charge[t] + # Losses associated with discharging + # Power drawn from storage is P_discharge / eta_d + # So discharge losses are (P_discharge / eta_d) - P_discharge + discharge_losses_kW = (1/eta_d - 1) * m.P_discharge[t] + + # Net change in SOC = (Energy_Charged_Gross - Charge_Losses) - (Energy_Discharged_Gross + Discharge_Losses) + # This is equivalent to: (eta_c * P_charge) - (P_discharge / eta_d) + + if t == 0: + # SOC[t] = soc_inicial + RECARGA_BRUTA - PERDAS_CARGA - (DESCARGA_BRUTA + PERDAS_DESCARGA) + return m.SOC[t] == soc_inicial + (m.P_charge[t] - charge_losses_kW) - (m.P_discharge[t] + discharge_losses_kW) + + # SOC[t] = SOC[t-1] + RECARGA_BRUTA - PERDAS_CARGA - (DESCARGA_BRUTA + PERDAS_DESCARGA) + return m.SOC[t] == m.SOC[t-1] + (m.P_charge[t] - charge_losses_kW) - (m.P_discharge[t] + discharge_losses_kW) + +# Reatribuindo (ou criando, se não existisse) a restrição soc_dyn no modelo +# O Pyomo pode emitir um WARNING se a restrição já existir, indicando que ela está sendo substituída. +model.soc_dyn = pyo.Constraint(model.T, rule=soc_rule) +# 5.3 Limites de SOC +model.soc_min = pyo.Constraint(model.T, rule=lambda m, t: m.SOC[t] >= soc_min) +model.soc_max = pyo.Constraint(model.T, rule=lambda m, t: m.SOC[t] <= soc_max) +#------------------------------------------------------- +# Alteração do código criado no para casa com a revisão do Guilherme +# -------------------------------------------------------- +model.soc_amanha = pyo.Constraint(expr= model.SOC[23] >= soc_inicial) #incluido junto do Guilherme para garantir que o BESS esteja minimamente carregado no final das 24h +# ----------------------------------------------------------- +# Continua versao do para casa antes do Guilherme +# 5.4 Limites de potência com binárias +model.charge_limit = pyo.Constraint( + model.T, rule=lambda m, t: m.P_charge[t] <= potencia_max_bess * m.u_charge[t] +) + +model.discharge_limit = pyo.Constraint( + model.T, rule=lambda m, t: m.P_discharge[t] <= potencia_max_bess * m.u_discharge[t] +) + +# 5.5 Bloqueio de carga e descarga simultâneas +model.no_simultaneous = pyo.Constraint( + model.T, rule=lambda m, t: m.u_charge[t] + m.u_discharge[t] <= 1 +) + + + +# ========================================================= +# 6. RESOLUÇÃO +# ========================================================= + +solver = pyo.SolverFactory("cbc") +results = solver.solve(model, tee=False) + +# ========================================================= +# 7. RESULTADOS +# ========================================================= + +# Define column names explicitly to avoid hidden character issues +col_demanda_comercio = "Demanda_Comercio".strip() +col_demanda_ev = "Demanda_EV".strip() +col_pv = "PV".strip() +col_grid = "Grid".strip() +col_export = "Export".strip() +col_carga_bess = "Carga_BESS".strip() +col_descarga_bess = "Descarga_BESS".strip() +col_soc = "SOC".strip() +col_hora = "Hora".strip() + +df = pd.DataFrame({ + col_hora: list(T), + col_demanda_comercio: demanda_comercio, + col_demanda_ev: demanda_ev, + col_pv: geracao_pv, + col_grid: [pyo.value(model.P_grid[t]) for t in T], + col_export: [pyo.value(model.P_export[t]) for t in T], + col_carga_bess: [pyo.value(model.P_charge[t]) for t in T], + col_descarga_bess: [pyo.value(model.P_discharge[t]) for t in T], + col_soc: [pyo.value(model.SOC[t]) for t in T] +}) + +print(df) +print("Custo total (R$):", pyo.value(model.OBJ)) + +print(f"Valor da Função Objetivo (Custo Total): R$ {pyo.value(model.OBJ):.2f}") + +# %% + +# ========================================================= +# 8. GRÁFICOS +# ========================================================= + +plt.figure() +plt.plot(df[col_hora], df[col_soc]) +plt.xlabel("Hora") +plt.ylabel("SOC (kWh)") +plt.title("Estado de Carga da Bateria") +plt.grid() +plt.show() + +# %% +# Define column names explicitly to avoid hidden character issues +col_demanda_comercio = "Demanda_Comercio".strip() +col_demanda_ev = "Demanda_EV".strip() +col_pv = "PV".strip() +col_grid = "Grid".strip() +col_export = "Export".strip() +col_carga_bess = "Carga_BESS".strip() +col_descarga_bess = "Descarga_BESS".strip() +col_soc = "SOC".strip() +col_hora = "Hora".strip() + +plt.figure(figsize=(14, 8)) + +# Plotando a demanda total +df['Demanda_Total'] = df[col_demanda_comercio] + df[col_demanda_ev] +plt.plot(df[col_hora], df['Demanda_Total'], label='Demanda Total', color='red', linestyle='--', linewidth=2) + +# Plotando as fontes de energia +plt.plot(df[col_hora], df[col_pv], label='Geração Fotovoltaica (PV)', color='green', marker='o', markersize=4) +plt.plot(df[col_hora], df[col_grid], label='Compra da Rede', color='blue', marker='x', markersize=4) +plt.plot(df[col_hora], df[col_descarga_bess], label='Descarga da Bateria', color='purple', marker='^', markersize=4) +plt.plot(df[col_hora], df[col_carga_bess], label='Carga da Bateria', color='orange', marker='v', markersize=4) +plt.plot(df[col_hora], df[col_export], label='Venda para a Rede', color='cyan', marker='s', markersize=4) + +plt.xlabel('Hora do Dia') +plt.ylabel('Potência (kW)') +plt.title('Fluxo de Carga do Sistema de Energia') +plt.legend() +plt.grid(True, linestyle=':', alpha=0.7) +plt.xticks(df[col_hora]) +plt.tight_layout() +plt.show() diff --git a/src/run_scenario.py b/src/run_scenario.py new file mode 100644 index 0000000..886a13a --- /dev/null +++ b/src/run_scenario.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import argparse +import csv +from pathlib import Path + +import pyomo.environ as pyo +import yaml + +from model import build_model + + +def load_config(path: Path) -> dict: + with path.open("r", encoding="utf-8") as f: + return yaml.safe_load(f) + + +def validate_config(cfg: dict) -> None: + n = int(cfg["horizonte_horas"]) + for key in ["demanda_comercio", "demanda_ev", "geracao_pv", "tarifa_compra", "tarifa_venda"]: + if len(cfg[key]) != n: + raise ValueError(f"'{key}' deve ter tamanho {n}.") + + +def solve(cfg: dict, solver_name: str = "cbc") -> pyo.ConcreteModel: + model = build_model(cfg) + solver = pyo.SolverFactory(solver_name) + result = solver.solve(model, tee=False) + if result.solver.termination_condition not in [pyo.TerminationCondition.optimal, pyo.TerminationCondition.feasible]: + raise RuntimeError(f"Solver terminou com status inesperado: {result.solver.termination_condition}") + return model + + +def export_results(model: pyo.ConcreteModel, cfg: dict, out_dir: Path) -> None: + out_dir.mkdir(parents=True, exist_ok=True) + path = out_dir / "dispatch.csv" + with path.open("w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["Hora", "Grid", "Export", "Carga_BESS", "Descarga_BESS", "SOC"]) + for t in model.T: + writer.writerow([ + t, + pyo.value(model.P_grid[t]), + pyo.value(model.P_export[t]), + pyo.value(model.P_charge[t]), + pyo.value(model.P_discharge[t]), + pyo.value(model.SOC[t]), + ]) + + summary = out_dir / "summary.txt" + summary.write_text( + f"scenario={cfg.get('name','sem_nome')}\nobjective={pyo.value(model.obj):.6f}\n", + encoding="utf-8", + ) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Executa cenário MILP de microgrid comercial.") + parser.add_argument("--config", required=True, type=Path, help="Caminho para YAML do cenário.") + parser.add_argument("--output", required=True, type=Path, help="Diretório de saída para resultados.") + parser.add_argument("--solver", default="cbc", help="Solver Pyomo (ex.: cbc, highs, gurobi).") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + cfg = load_config(args.config) + validate_config(cfg) + model = solve(cfg, solver_name=args.solver) + export_results(model, cfg, args.output) + print(f"✅ Cenário '{cfg.get('name', 'sem_nome')}' executado. Resultados em: {args.output}") + + +if __name__ == "__main__": + main()