diff --git a/CHANGELOG.md b/CHANGELOG.md index 61543ba..47e98c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ O formato segue o [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/). ## [Nao Publicado] +## [1.12.0] - 2026-07-24 + +### Adicionado + +- Leitor do arquivo `mlt.dat`, com as médias mensais de longo termo (MLT) de cada posto por mês do ano, através da nova classe `Mlt`. O conteúdo é exposto como um `DataFrame` na propriedade `valores`, com uma linha por mês e uma coluna por posto, em m³/s. +- Leitor do arquivo `renovaveis.csv`, com o cadastro dos parques eólicos equivalentes, através da nova classe `Renovaveis` e dos registros da família `PEE` (`PEECadastro`, `PEESubmercado`, `PEEConfiguracaoPeriodo`, `PEEPotenciaInstaladaPeriodo` e `PEEGeracaoPeriodoPatamarCenario`). + +### Alterado + +- Versão mínima de `cfinterface` elevada para `1.10.1`. + +### Corrigido + +- O registro `IA` do `dadger` lia os limites de intercâmbio por patamar, mas não os expunha, de forma que `ia(df=True)` os omitia. Adicionadas as propriedades `limite_de_para` e `limite_para_de`. +- O método `ue` do `Dadger` retornava os dados do registro `UH` em vez do registro `UE`. +- Precisão decimal insuficiente (`decimal_digits=0`) nos limites de intercâmbio do registro `IA`, que arredondava para inteiro valores fracionários na escrita (round-trip). Ajustada para `decimal_digits=2`. + ## [1.11.0] - 2026-06-09 ### Adicionado diff --git a/docs/source/referencia/decomp/arquivos/mlt.rst b/docs/source/referencia/decomp/arquivos/mlt.rst new file mode 100644 index 0000000..3ea8bd5 --- /dev/null +++ b/docs/source/referencia/decomp/arquivos/mlt.rst @@ -0,0 +1,13 @@ +.. _mlt: + +============================================= +MLT (mlt.dat) +============================================= + +.. currentmodule:: idecomp.decomp.mlt + +As médias mensais de longo termo (MLT) por posto do DECOMP, localizadas no arquivo geralmente denominado +mlt.dat, são armazenadas na classe: + +.. autoclass:: Mlt + :members: diff --git a/docs/source/referencia/decomp/index.rst b/docs/source/referencia/decomp/index.rst index 0e6c8a7..ded4be4 100644 --- a/docs/source/referencia/decomp/index.rst +++ b/docs/source/referencia/decomp/index.rst @@ -39,6 +39,7 @@ DECOMP idecomp.decomp.hidr.Hidr idecomp.decomp.inviabunic.InviabUnic idecomp.decomp.mapcut.Mapcut + idecomp.decomp.mlt.Mlt idecomp.decomp.oper_desvio_fpha.OperDesvioFpha idecomp.decomp.oper_disp_usih.OperDispUsih idecomp.decomp.oper_disp_usih_ree.OperDispUsihRee @@ -84,6 +85,7 @@ DECOMP arquivos/hidr arquivos/inviabunic arquivos/mapcut + arquivos/mlt arquivos/oper_desvio_fpha arquivos/oper_disp_usih arquivos/oper_disp_usih_ree diff --git a/docs/source/referencia/libs/arquivos/renovaveis.rst b/docs/source/referencia/libs/arquivos/renovaveis.rst new file mode 100644 index 0000000..652e728 --- /dev/null +++ b/docs/source/referencia/libs/arquivos/renovaveis.rst @@ -0,0 +1,15 @@ +.. _renovaveis: + +======================================================= +Dados das Usinas Renováveis +======================================================= + +.. currentmodule:: idecomp.libs.renovaveis + +As informações de usinas renováveis (parques eólicos +equivalentes) no DECOMP, localizadas no arquivos das LIBS +que são indicados no indices.csv com as respectivas +funcionalidades, são armazenados na classe: + +.. autoclass:: Renovaveis + :members: diff --git a/docs/source/referencia/libs/index.rst b/docs/source/referencia/libs/index.rst index 9f0cd06..16ece33 100644 --- a/docs/source/referencia/libs/index.rst +++ b/docs/source/referencia/libs/index.rst @@ -6,11 +6,13 @@ LIBS .. autosummary:: :nosignatures: + idecomp.libs.renovaveis.Renovaveis idecomp.libs.restricoes.Restricoes idecomp.libs.usinas_hidreletricas.UsinasHidreletricas .. toctree:: :maxdepth: 2 + arquivos/renovaveis arquivos/restricoes arquivos/usinas_hidreletricas diff --git a/idecomp/__init__.py b/idecomp/__init__.py index aac56f7..ce91b55 100644 --- a/idecomp/__init__.py +++ b/idecomp/__init__.py @@ -6,6 +6,6 @@ e saída do DECOMP. """ -__version__ = "1.11.0" +__version__ = "1.12.0" from . import decomp # noqa diff --git a/idecomp/decomp/__init__.py b/idecomp/decomp/__init__.py index 1195b9a..ef0a226 100644 --- a/idecomp/decomp/__init__.py +++ b/idecomp/decomp/__init__.py @@ -33,6 +33,7 @@ from .hidr import Hidr # noqa from .inviabunic import InviabUnic # noqa from .mapcut import Mapcut # noqa +from .mlt import Mlt # noqa from .oper_desvio_fpha import OperDesvioFpha # noqa from .oper_disp_usih_ree import OperDispUsihRee # noqa from .oper_disp_usih_subm import OperDispUsihSubm # noqa diff --git a/idecomp/decomp/dadger.py b/idecomp/decomp/dadger.py index 7fde43b..0d5d46c 100644 --- a/idecomp/decomp/dadger.py +++ b/idecomp/decomp/dadger.py @@ -356,7 +356,7 @@ def ue( :rtype: :class:`UE` | list[:class:`UE`] | :class:`pandas.DataFrame` | None """ return self.__registros_ou_df( - UH, + UE, codigo_usina=codigo_usina, codigo_submercado=codigo_submercado, nome_usina=nome_usina, diff --git a/idecomp/decomp/mlt.py b/idecomp/decomp/mlt.py new file mode 100644 index 0000000..26628ac --- /dev/null +++ b/idecomp/decomp/mlt.py @@ -0,0 +1,75 @@ +from typing import IO, Any, TypeVar + +import numpy as np +import pandas as pd # type: ignore[import-untyped] +from cfinterface.files.sectionfile import SectionFile + +from idecomp.decomp.modelos.mlt import SecaoMlt + + +class Mlt(SectionFile): + """ + Armazena os dados de entrada do DECOMP referentes às médias + mensais de longo termo (MLT) de cada posto. + """ + + T = TypeVar("T") + + SECTIONS = [SecaoMlt] + STORAGE = "BINARY" + + def __init__(self, data: Any = ...) -> None: + super().__init__(data) + self.__df: pd.DataFrame | None = None + + def write(self, to: str | IO[Any], *args: Any, **kwargs: Any) -> None: + self.__atualiza_secao() + super().write(to, *args, **kwargs) + + def __obtem_secao_mlt(self) -> SecaoMlt | None: + s = self.data.get_sections_of_type(SecaoMlt) + return s if not isinstance(s, list) else None + + def __atualiza_secao(self) -> None: + dados = self.__obtem_secao_mlt() + if dados is not None and self.__df is not None: + cols_postos = [str(p) for p in range(1, SecaoMlt.NUMERO_POSTOS + 1)] + dados.data = self.__df[cols_postos].to_numpy().flatten().tolist() + + @property + def valores(self) -> pd.DataFrame | None: + """ + Obtém a tabela com as médias mensais de longo termo (MLT) de + cada posto, por mês do ano. + + - mes (`int`) + - 1 (`int`) + - ... + - N (`int`) : número de postos + + :return: A tabela com as médias por mês e posto + :rtype: pd.DataFrame | None + """ + if self.__df is None: + dados = self.__obtem_secao_mlt() + if dados is not None: + cols_postos = [ + str(p) for p in range(1, SecaoMlt.NUMERO_POSTOS + 1) + ] + df = pd.DataFrame( + np.array(dados.data).reshape( + (SecaoMlt.NUMERO_MESES, SecaoMlt.NUMERO_POSTOS) + ), + columns=cols_postos, + ) + df["mes"] = list(range(1, SecaoMlt.NUMERO_MESES + 1)) + self.__df = df[["mes"] + cols_postos] + return self.__df + + @valores.setter + def valores(self, df: pd.DataFrame) -> None: + dados = self.__obtem_secao_mlt() + if dados is not None: + cols_postos = [str(p) for p in range(1, SecaoMlt.NUMERO_POSTOS + 1)] + dados.data = df[cols_postos].to_numpy().flatten().tolist() + self.__df = None diff --git a/idecomp/decomp/modelos/dadger.py b/idecomp/decomp/modelos/dadger.py index 3d30dec..546691e 100644 --- a/idecomp/decomp/modelos/dadger.py +++ b/idecomp/decomp/modelos/dadger.py @@ -1051,19 +1051,34 @@ class IA(Register): LiteralField(2, 9), LiteralField(2, 14), IntegerField(1, 17), - FloatField(10, 19, 0), - FloatField(10, 29, 0), - FloatField(10, 39, 0), - FloatField(10, 49, 0), - FloatField(10, 59, 0), - FloatField(10, 69, 0), - FloatField(10, 79, 0), - FloatField(10, 89, 0), - FloatField(10, 99, 0), - FloatField(10, 109, 0), + FloatField(10, 19, 2), + FloatField(10, 29, 2), + FloatField(10, 39, 2), + FloatField(10, 49, 2), + FloatField(10, 59, 2), + FloatField(10, 69, 2), + FloatField(10, 79, 2), + FloatField(10, 89, 2), + FloatField(10, 99, 2), + FloatField(10, 109, 2), ] ) + def __atualiza_dados_lista( + self, + novos_dados: list[Any], + indice_inicial: int, + espacamento: int, + ) -> None: + atuais = len(self.data) + ultimo_indice = indice_inicial + espacamento * len(novos_dados) + diferenca = (ultimo_indice - atuais) // espacamento + if diferenca > 0: + self.data += [None] * (ultimo_indice - atuais) + diferenca -= 1 + novos_dados += [None] * abs(diferenca) + self.data[indice_inicial::espacamento] = novos_dados + @property def estagio(self) -> int | None: """ @@ -1106,6 +1121,34 @@ def nome_submercado_para(self) -> str | None: def nome_submercado_para(self, t: str) -> None: self.data[2] = t + @property + def limite_de_para(self) -> list[float] | None: + """ + Os limites de intercâmbio no sentido de -> para, por patamar. + + :return: Os limites por patamar. + :rtype: list[float] | None + """ + return self.data[4::2] + + @limite_de_para.setter + def limite_de_para(self, lim: list[float]) -> None: + self.__atualiza_dados_lista(lim, 4, 2) + + @property + def limite_para_de(self) -> list[float] | None: + """ + Os limites de intercâmbio no sentido para -> de, por patamar. + + :return: Os limites por patamar. + :rtype: list[float] | None + """ + return self.data[5::2] + + @limite_para_de.setter + def limite_para_de(self, lim: list[float]) -> None: + self.__atualiza_dados_lista(lim, 5, 2) + class TX(Register): """ diff --git a/idecomp/decomp/modelos/mlt.py b/idecomp/decomp/modelos/mlt.py new file mode 100644 index 0000000..8f696bf --- /dev/null +++ b/idecomp/decomp/modelos/mlt.py @@ -0,0 +1,42 @@ +from typing import IO, Any + +import numpy as np +from cfinterface.components.section import Section + + +class SecaoMlt(Section): + """ + Registro com os dados das médias mensais de longo termo (MLT) + de cada posto, por mês do ano. + """ + + __slots__: list[str] = [] + + NUMERO_MESES = 12 + NUMERO_POSTOS = 320 + + def __init__( + self, + previous: Any = None, + next: Any = None, + data: Any = None, + ) -> None: + super().__init__(previous, next, data) + self.data = [] + + def __eq__(self, o: object) -> bool: + if not isinstance(o, SecaoMlt): + return False + bloco: SecaoMlt = o + return self.data == bloco.data + + def read(self, file: IO[Any], *args: Any, **kwargs: Any) -> None: # type: ignore[override] + dados = np.frombuffer( + file.read(SecaoMlt.NUMERO_MESES * SecaoMlt.NUMERO_POSTOS * 4), + dtype=np.int32, + count=SecaoMlt.NUMERO_MESES * SecaoMlt.NUMERO_POSTOS, + ) + self.data = list(dados) + + def write(self, file: IO[Any], *args: Any, **kwargs: Any) -> None: # type: ignore[override] + file.write(np.array(self.data, dtype=np.int32).tobytes()) diff --git a/idecomp/libs/__init__.py b/idecomp/libs/__init__.py index af9a4b7..b3b9fdb 100644 --- a/idecomp/libs/__init__.py +++ b/idecomp/libs/__init__.py @@ -1,3 +1,4 @@ # Inclui os membros +from .renovaveis import Renovaveis # noqa from .usinas_hidreletricas import UsinasHidreletricas # noqa diff --git a/idecomp/libs/modelos/renovaveis.py b/idecomp/libs/modelos/renovaveis.py new file mode 100644 index 0000000..30c6f48 --- /dev/null +++ b/idecomp/libs/modelos/renovaveis.py @@ -0,0 +1,345 @@ +from cfinterface.components.floatfield import FloatField +from cfinterface.components.integerfield import IntegerField +from cfinterface.components.line import Line +from cfinterface.components.literalfield import LiteralField +from cfinterface.components.register import Register + + +class PEECadastro(Register): + """ """ + + __slots__ = [] + + IDENTIFIER = "PEE-CAD" + IDENTIFIER_DIGITS = 8 + LINE = Line( + [ + IntegerField(), + LiteralField(), + ], + delimiter=";", + ) + + @property + def codigo_pee(self) -> int | None: + """ + O código do parque eólico equivalente. + + :return: O código + :rtype: int | None + """ + return self.data[0] + + @codigo_pee.setter + def codigo_pee(self, c: int) -> None: + self.data[0] = c + + @property + def nome_pee(self) -> str | None: + """ + O nome do parque eólico equivalente. + + :return: O nome + :rtype: str | None + """ + return self.data[1] + + @nome_pee.setter + def nome_pee(self, n: str) -> None: + self.data[1] = n + + +class PEESubmercado(Register): + """ """ + + __slots__ = [] + + IDENTIFIER = "PEE-SUBM" + IDENTIFIER_DIGITS = 9 + LINE = Line( + [ + IntegerField(), + IntegerField(), + ], + delimiter=";", + ) + + @property + def codigo_pee(self) -> int | None: + """ + O código do parque eólico equivalente. + + :return: O código + :rtype: int | None + """ + return self.data[0] + + @codigo_pee.setter + def codigo_pee(self, c: int) -> None: + self.data[0] = c + + @property + def codigo_submercado(self) -> int | None: + """ + O código do submercado do parque eólico equivalente. + + :return: O código + :rtype: int | None + """ + return self.data[1] + + @codigo_submercado.setter + def codigo_submercado(self, c: int) -> None: + self.data[1] = c + + +class PEEConfiguracaoPeriodo(Register): + """ """ + + __slots__ = [] + + IDENTIFIER = "PEE-CONFIG-PER" + IDENTIFIER_DIGITS = 15 + LINE = Line( + [ + IntegerField(), + IntegerField(), + IntegerField(), + LiteralField(), + ], + delimiter=";", + ) + + @property + def codigo_pee(self) -> int | None: + """ + O código do parque eólico equivalente. + + :return: O código + :rtype: int | None + """ + return self.data[0] + + @codigo_pee.setter + def codigo_pee(self, c: int) -> None: + self.data[0] = c + + @property + def estagio_inicial(self) -> int | None: + """ + O estágio inicial de validade do estado de operação. + + :return: O estágio + :rtype: int | None + """ + return self.data[1] + + @estagio_inicial.setter + def estagio_inicial(self, e: int) -> None: + self.data[1] = e + + @property + def estagio_final(self) -> int | None: + """ + O estágio final de validade do estado de operação. + + :return: O estágio + :rtype: int | None + """ + return self.data[2] + + @estagio_final.setter + def estagio_final(self, e: int) -> None: + self.data[2] = e + + @property + def estado_operacao(self) -> str | None: + """ + O estado de operação do parque eólico equivalente. + + :return: O estado + :rtype: str | None + """ + return self.data[3] + + @estado_operacao.setter + def estado_operacao(self, e: str) -> None: + self.data[3] = e + + +class PEEPotenciaInstaladaPeriodo(Register): + """ """ + + __slots__ = [] + + IDENTIFIER = "PEE-POT-INST-PER" + IDENTIFIER_DIGITS = 17 + LINE = Line( + [ + IntegerField(), + IntegerField(), + IntegerField(), + FloatField(decimal_digits=3), + ], + delimiter=";", + ) + + @property + def codigo_pee(self) -> int | None: + """ + O código do parque eólico equivalente. + + :return: O código + :rtype: int | None + """ + return self.data[0] + + @codigo_pee.setter + def codigo_pee(self, c: int) -> None: + self.data[0] = c + + @property + def estagio_inicial(self) -> int | None: + """ + O estágio inicial de validade da potência instalada. + + :return: O estágio + :rtype: int | None + """ + return self.data[1] + + @estagio_inicial.setter + def estagio_inicial(self, e: int) -> None: + self.data[1] = e + + @property + def estagio_final(self) -> int | None: + """ + O estágio final de validade da potência instalada. + + :return: O estágio + :rtype: int | None + """ + return self.data[2] + + @estagio_final.setter + def estagio_final(self, e: int) -> None: + self.data[2] = e + + @property + def potencia_instalada(self) -> float | None: + """ + A potência instalada do parque eólico equivalente. + + :return: A potência em MW + :rtype: float | None + """ + return self.data[3] + + @potencia_instalada.setter + def potencia_instalada(self, p: float) -> None: + self.data[3] = p + + +class PEEGeracaoPeriodoPatamarCenario(Register): + """ """ + + __slots__ = [] + + IDENTIFIER = "PEE-GER-PER-PAT-CEN" + IDENTIFIER_DIGITS = 20 + LINE = Line( + [ + IntegerField(), + IntegerField(), + IntegerField(), + IntegerField(), + IntegerField(), + FloatField(decimal_digits=4), + ], + delimiter=";", + ) + + @property + def codigo_pee(self) -> int | None: + """ + O código do parque eólico equivalente. + + :return: O código + :rtype: int | None + """ + return self.data[0] + + @codigo_pee.setter + def codigo_pee(self, c: int) -> None: + self.data[0] = c + + @property + def estagio_inicial(self) -> int | None: + """ + O estágio inicial de validade da geração. + + :return: O estágio + :rtype: int | None + """ + return self.data[1] + + @estagio_inicial.setter + def estagio_inicial(self, e: int) -> None: + self.data[1] = e + + @property + def estagio_final(self) -> int | None: + """ + O estágio final de validade da geração. + + :return: O estágio + :rtype: int | None + """ + return self.data[2] + + @estagio_final.setter + def estagio_final(self, e: int) -> None: + self.data[2] = e + + @property + def patamar(self) -> int | None: + """ + O índice do patamar de carga. + + :return: O patamar + :rtype: int | None + """ + return self.data[3] + + @patamar.setter + def patamar(self, p: int) -> None: + self.data[3] = p + + @property + def cenario(self) -> int | None: + """ + O índice do cenário. + + :return: O cenário + :rtype: int | None + """ + return self.data[4] + + @cenario.setter + def cenario(self, c: int) -> None: + self.data[4] = c + + @property + def geracao(self) -> float | None: + """ + A geração do parque eólico equivalente. + + :return: A geração em MW + :rtype: float | None + """ + return self.data[5] + + @geracao.setter + def geracao(self, g: float) -> None: + self.data[5] = g diff --git a/idecomp/libs/renovaveis.py b/idecomp/libs/renovaveis.py new file mode 100644 index 0000000..57480f3 --- /dev/null +++ b/idecomp/libs/renovaveis.py @@ -0,0 +1,235 @@ +from typing import Any, TypeVar + +import pandas as pd # type: ignore[import-untyped] +from cfinterface.components.register import Register +from cfinterface.files.registerfile import RegisterFile + +from idecomp.libs.modelos.renovaveis import ( + PEECadastro, + PEEConfiguracaoPeriodo, + PEEGeracaoPeriodoPatamarCenario, + PEEPotenciaInstaladaPeriodo, + PEESubmercado, +) + + +class Renovaveis(RegisterFile): + """ + Armazena os dados de entrada do DECOMP referentes aos dados + das usinas renováveis (parques eólicos equivalentes) do problema. + """ + + T = TypeVar("T", bound=Register) + + REGISTERS = [ + PEEGeracaoPeriodoPatamarCenario, + PEEPotenciaInstaladaPeriodo, + PEEConfiguracaoPeriodo, + PEESubmercado, + PEECadastro, + ] + + def __init__(self, data: Any = ...) -> None: + super().__init__(data) + + def __registros_ou_df( + self, t: type[T], **kwargs: Any + ) -> T | list[T] | pd.DataFrame | None: + if kwargs.get("df"): + return self._as_df(t) + else: + kwargs_sem_df = {k: v for k, v in kwargs.items() if k != "df"} + return self.data.get_registers_of_type(t, **kwargs_sem_df) + + def pee_cad( + self, + codigo_pee: int | None = None, + nome_pee: str | None = None, + df: bool = False, + ) -> PEECadastro | list[PEECadastro] | pd.DataFrame | None: + """ + Obtém registros que cadastram um parque eólico equivalente. + Opcionalmente, o retorno pode ser transformado em um `DataFrame`, + apenas para leitura das informações. + + :param codigo_pee: código que especifica o parque + :type codigo_pee: int | None + :param nome_pee: nome do parque + :type nome_pee: str | None + :param df: ignorar os filtros e retornar + todos os dados de registros como um DataFrame + :type df: bool + :return: Um ou mais registros, se existirem. + :rtype: `PEECadastro` | list[`PEECadastro`] | `None` | `DataFrame` + """ + return self.__registros_ou_df( + PEECadastro, + codigo_pee=codigo_pee, + nome_pee=nome_pee, + df=df, + ) + + def pee_subm( + self, + codigo_pee: int | None = None, + codigo_submercado: int | None = None, + df: bool = False, + ) -> PEESubmercado | list[PEESubmercado] | pd.DataFrame | None: + """ + Obtém registros que relacionam um parque eólico equivalente + a um submercado. Opcionalmente, o retorno pode ser transformado + em um `DataFrame`, apenas para leitura das informações. + + :param codigo_pee: código que especifica o parque + :type codigo_pee: int | None + :param codigo_submercado: código do submercado + :type codigo_submercado: int | None + :param df: ignorar os filtros e retornar + todos os dados de registros como um DataFrame + :type df: bool + :return: Um ou mais registros, se existirem. + :rtype: `PEESubmercado` | list[`PEESubmercado`] | `None` | `DataFrame` + """ + return self.__registros_ou_df( + PEESubmercado, + codigo_pee=codigo_pee, + codigo_submercado=codigo_submercado, + df=df, + ) + + def pee_config_per( + self, + codigo_pee: int | None = None, + estagio_inicial: int | None = None, + estagio_final: int | None = None, + estado_operacao: str | None = None, + df: bool = False, + ) -> ( + PEEConfiguracaoPeriodo + | list[PEEConfiguracaoPeriodo] + | pd.DataFrame + | None + ): + """ + Obtém registros que definem a configuração de operação de um + parque eólico equivalente por período. Opcionalmente, o retorno + pode ser transformado em um `DataFrame`, apenas para leitura das + informações. + + :param codigo_pee: código que especifica o parque + :type codigo_pee: int | None + :param estagio_inicial: estágio inicial de validade + :type estagio_inicial: int | None + :param estagio_final: estágio final de validade + :type estagio_final: int | None + :param estado_operacao: estado de operação do parque + :type estado_operacao: str | None + :param df: ignorar os filtros e retornar + todos os dados de registros como um DataFrame + :type df: bool + :return: Um ou mais registros, se existirem. + :rtype: `PEEConfiguracaoPeriodo` | list[`PEEConfiguracaoPeriodo`] | + `None` | `DataFrame` + """ + return self.__registros_ou_df( + PEEConfiguracaoPeriodo, + codigo_pee=codigo_pee, + estagio_inicial=estagio_inicial, + estagio_final=estagio_final, + estado_operacao=estado_operacao, + df=df, + ) + + def pee_pot_inst_per( + self, + codigo_pee: int | None = None, + estagio_inicial: int | None = None, + estagio_final: int | None = None, + potencia_instalada: float | None = None, + df: bool = False, + ) -> ( + PEEPotenciaInstaladaPeriodo + | list[PEEPotenciaInstaladaPeriodo] + | pd.DataFrame + | None + ): + """ + Obtém registros que definem a potência instalada de um parque + eólico equivalente por período. Opcionalmente, o retorno pode ser + transformado em um `DataFrame`, apenas para leitura das + informações. + + :param codigo_pee: código que especifica o parque + :type codigo_pee: int | None + :param estagio_inicial: estágio inicial de validade + :type estagio_inicial: int | None + :param estagio_final: estágio final de validade + :type estagio_final: int | None + :param potencia_instalada: potência instalada do parque + :type potencia_instalada: float | None + :param df: ignorar os filtros e retornar + todos os dados de registros como um DataFrame + :type df: bool + :return: Um ou mais registros, se existirem. + :rtype: `PEEPotenciaInstaladaPeriodo` | + list[`PEEPotenciaInstaladaPeriodo`] | `None` | `DataFrame` + """ + return self.__registros_ou_df( + PEEPotenciaInstaladaPeriodo, + codigo_pee=codigo_pee, + estagio_inicial=estagio_inicial, + estagio_final=estagio_final, + potencia_instalada=potencia_instalada, + df=df, + ) + + def pee_ger_per_pat_cen( + self, + codigo_pee: int | None = None, + estagio_inicial: int | None = None, + estagio_final: int | None = None, + patamar: int | None = None, + cenario: int | None = None, + geracao: float | None = None, + df: bool = False, + ) -> ( + PEEGeracaoPeriodoPatamarCenario + | list[PEEGeracaoPeriodoPatamarCenario] + | pd.DataFrame + | None + ): + """ + Obtém registros que definem a geração de um parque eólico + equivalente por período, patamar e cenário. Opcionalmente, o + retorno pode ser transformado em um `DataFrame`, apenas para + leitura das informações. + + :param codigo_pee: código que especifica o parque + :type codigo_pee: int | None + :param estagio_inicial: estágio inicial de validade + :type estagio_inicial: int | None + :param estagio_final: estágio final de validade + :type estagio_final: int | None + :param patamar: índice do patamar de carga + :type patamar: int | None + :param cenario: índice do cenário + :type cenario: int | None + :param geracao: geração do parque + :type geracao: float | None + :param df: ignorar os filtros e retornar + todos os dados de registros como um DataFrame + :type df: bool + :return: Um ou mais registros, se existirem. + :rtype: `PEEGeracaoPeriodoPatamarCenario` | + list[`PEEGeracaoPeriodoPatamarCenario`] | `None` | `DataFrame` + """ + return self.__registros_ou_df( + PEEGeracaoPeriodoPatamarCenario, + codigo_pee=codigo_pee, + estagio_inicial=estagio_inicial, + estagio_final=estagio_final, + patamar=patamar, + cenario=cenario, + geracao=geracao, + df=df, + ) diff --git a/pyproject.toml b/pyproject.toml index b581e75..cf7700f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "idecomp" dynamic = ["version"] dependencies = [ - "cfinterface>=1.10.0", + "cfinterface>=1.10.1", "numpy>=2.2.1", "pandas>=3.0.0", ] @@ -32,7 +32,7 @@ classifiers = [ [project.optional-dependencies] test = ["pytest", "pytest-cov", "pytest-xdist"] -lint = ["mypy", "pre-commit", "ruff"] +lint = ["mypy", "pre-commit", "ruff<0.16"] docs = [ "sphinx", "furo", diff --git a/tests/decomp/test_dadger.py b/tests/decomp/test_dadger.py index 299f97d..1e9949c 100644 --- a/tests/decomp/test_dadger.py +++ b/tests/decomp/test_dadger.py @@ -490,6 +490,33 @@ def test_registro_ia_dadger(): None, None, ] + assert r.limite_de_para == [4200, 4200, 4200, None, None] + r.limite_de_para = [None, None, None, None, None] + assert r.limite_de_para == [None, None, None, None, None] + assert r.limite_para_de == [5500, 5500, 5500, None, None] + r.limite_para_de = [None, None, None, None, None] + assert r.limite_para_de == [None, None, None, None, None] + + +def test_escrita_ia_limites_fracionarios_dadger(): + r = IA() + r.estagio = 1 + r.nome_submercado_de = "SE" + r.nome_submercado_para = "SU" + r.limite_de_para = [6800.5, None, None, None, None] + r.limite_para_de = [9800.25, None, None, None, None] + m: MagicMock = mock_open(read_data="") + with patch("builtins.open", m): + with open("", "w") as fp: + r.write(fp) + linha = m().write.call_args.args[0] + m2: MagicMock = mock_open(read_data=linha) + r2 = IA() + with patch("builtins.open", m2): + with open("", "") as fp: + r2.read(fp) + assert r2.limite_de_para[0] == 6800.5 + assert r2.limite_para_de[0] == 9800.25 def test_registro_tx_dadger(): @@ -952,6 +979,7 @@ def test_registro_accotvol_dadger(): r.ano = 2022 assert r.ano == 2022 + def test_registro_accotare_dadger(): m: MagicMock = mock_open(read_data="".join(MockACCOTARE)) r = ACCOTARE() @@ -2119,6 +2147,7 @@ def test_campos_encontrados_dadger(): assert d.te is not None assert d.sb(1) is not None assert d.uh(1) is not None + assert d.ue(1) is not None assert d.ct(65, 1) is not None assert d.dp(1, 1) is not None assert d.ac(285, ACJUSMED) is not None @@ -2148,6 +2177,10 @@ def test_campos_encontrados_dadger(): assert len(d.lq(df=True).columns) == 12 assert d.ia(1) is not None assert d.ri(66) is not None + assert len(d.ue(df=True)) == 4 + assert "taxa_consumo" in d.ue(df=True).columns + assert "limite_de_para_1" in d.ia(df=True).columns + assert "limite_para_de_5" in d.ia(df=True).columns def test_cria_lu_dadger(): diff --git a/tests/decomp/test_mlt.py b/tests/decomp/test_mlt.py new file mode 100644 index 0000000..1df8d49 --- /dev/null +++ b/tests/decomp/test_mlt.py @@ -0,0 +1,64 @@ +from idecomp.decomp.modelos.mlt import SecaoMlt +from idecomp.decomp.mlt import Mlt + +from os.path import join +from tests.mocks.mock_open import mock_open +from unittest.mock import MagicMock, patch + + +ARQ_TEST = "./tests/mocks/arquivos/mlt.dat" + + +def test_secao_mlt(): + r = SecaoMlt() + with open(join(ARQ_TEST), "rb") as fp: + r.read(fp, storage="BINARY") + + assert len(r.data) == SecaoMlt.NUMERO_MESES * SecaoMlt.NUMERO_POSTOS + assert r.data[0] == 242 + assert r.data[65] == 14678 + + +def test_atributos_encontrados_mlt(): + h = Mlt.read(ARQ_TEST) + assert h.valores is not None + + +def test_valores_mlt(): + h = Mlt.read(ARQ_TEST) + df = h.valores + assert df["mes"].tolist() == list(range(1, 13)) + assert df.at[0, "1"] == 242 + assert df.at[11, "1"] == 174 + assert df.at[0, "66"] == 14678 + + +def test_eq_mlt(): + h1 = Mlt.read(ARQ_TEST) + h2 = Mlt.read(ARQ_TEST) + assert h1 == h2 + + +def test_neq_mlt(): + h1 = Mlt.read(ARQ_TEST) + h2 = Mlt.read(ARQ_TEST) + df = h2.valores + df["1"] = -1 + h2.valores = df + assert h1 != h2 + + +def test_leitura_escrita_mlt(): + h1 = Mlt.read(ARQ_TEST) + m_escrita: MagicMock = mock_open(read_data="") + with patch("builtins.open", m_escrita): + h1.write(ARQ_TEST) + # Recupera o que foi escrito + chamadas = m_escrita.mock_calls + linhas_escritas = [ + chamadas[i].args[0] for i in range(1, len(chamadas) - 1) + ] + m_releitura: MagicMock = mock_open(read_data=b"".join(linhas_escritas)) + with patch("builtins.open", m_releitura): + h2 = Mlt.read(ARQ_TEST) + assert h1 == h2 diff --git a/tests/libs/test_renovaveis.py b/tests/libs/test_renovaveis.py new file mode 100644 index 0000000..bc21833 --- /dev/null +++ b/tests/libs/test_renovaveis.py @@ -0,0 +1,219 @@ +from idecomp.libs.renovaveis import Renovaveis +from idecomp.libs.modelos.renovaveis import ( + PEECadastro, + PEESubmercado, + PEEConfiguracaoPeriodo, + PEEPotenciaInstaladaPeriodo, + PEEGeracaoPeriodoPatamarCenario, +) +from tests.mocks.mock_open import mock_open +from unittest.mock import MagicMock, patch + +from tests.mocks.arquivos.renovaveis import ( + MockRenovaveis, + MockPEECadastro, + MockPEESubmercado, + MockPEEConfiguracaoPeriodo, + MockPEEPotenciaInstaladaPeriodo, + MockPEEGeracaoPeriodoPatamarCenario, +) + +ARQ_TESTE = "./tests/__init__.py" + + +def test_atributos_encontrados_renovaveis(): + m: MagicMock = mock_open(read_data="".join(MockRenovaveis)) + with patch("builtins.open", m): + renovaveis = Renovaveis.read(ARQ_TESTE) + assert renovaveis.pee_cad() is not None + assert renovaveis.pee_subm() is not None + assert renovaveis.pee_config_per() is not None + assert renovaveis.pee_pot_inst_per() is not None + assert renovaveis.pee_ger_per_pat_cen() is not None + + +def test_df_renovaveis_pee_cad(): + m: MagicMock = mock_open(read_data="".join(MockRenovaveis)) + with patch("builtins.open", m): + renovaveis = Renovaveis.read(ARQ_TESTE) + df = renovaveis.pee_cad(df=True) + assert len(df) == 8 + assert df.at[0, "codigo_pee"] == 1 + assert df.at[0, "nome_pee"] == "Eolica SECO" + + +def test_registro_renovaveis_pee_cad(): + m: MagicMock = mock_open(read_data="".join(MockPEECadastro)) + r = PEECadastro() + with patch("builtins.open", m): + with open("", "") as fp: + r.read(fp) + + assert r.data == [1, "Eolica SECO"] + assert r.codigo_pee == 1 + r.codigo_pee = 0 + assert r.codigo_pee == 0 + assert r.nome_pee == "Eolica SECO" + r.nome_pee = "Eolica S" + assert r.nome_pee == "Eolica S" + + +def test_df_renovaveis_pee_subm(): + m: MagicMock = mock_open(read_data="".join(MockRenovaveis)) + with patch("builtins.open", m): + renovaveis = Renovaveis.read(ARQ_TESTE) + df = renovaveis.pee_subm(df=True) + assert len(df) == 8 + assert df.at[0, "codigo_pee"] == 1 + assert df.at[0, "codigo_submercado"] == 1 + + +def test_registro_renovaveis_pee_subm(): + m: MagicMock = mock_open(read_data="".join(MockPEESubmercado)) + r = PEESubmercado() + with patch("builtins.open", m): + with open("", "") as fp: + r.read(fp) + + assert r.data == [1, 1] + assert r.codigo_pee == 1 + r.codigo_pee = 0 + assert r.codigo_pee == 0 + assert r.codigo_submercado == 1 + r.codigo_submercado = 0 + assert r.codigo_submercado == 0 + + +def test_df_renovaveis_pee_config_per(): + m: MagicMock = mock_open(read_data="".join(MockRenovaveis)) + with patch("builtins.open", m): + renovaveis = Renovaveis.read(ARQ_TESTE) + df = renovaveis.pee_config_per(df=True) + assert len(df) == 8 + assert df.at[0, "codigo_pee"] == 1 + assert df.at[0, "estagio_inicial"] == 1 + assert df.at[0, "estagio_final"] == 3 + assert df.at[0, "estado_operacao"] == "centralizado" + + +def test_registro_renovaveis_pee_config_per(): + m: MagicMock = mock_open(read_data="".join(MockPEEConfiguracaoPeriodo)) + r = PEEConfiguracaoPeriodo() + with patch("builtins.open", m): + with open("", "") as fp: + r.read(fp) + + assert r.data == [1, 1, 3, "centralizado"] + assert r.codigo_pee == 1 + r.codigo_pee = 0 + assert r.codigo_pee == 0 + assert r.estagio_inicial == 1 + r.estagio_inicial = 0 + assert r.estagio_inicial == 0 + assert r.estagio_final == 3 + r.estagio_final = 0 + assert r.estagio_final == 0 + assert r.estado_operacao == "centralizado" + r.estado_operacao = "descentralizado" + assert r.estado_operacao == "descentralizado" + + +def test_df_renovaveis_pee_pot_inst_per(): + m: MagicMock = mock_open(read_data="".join(MockRenovaveis)) + with patch("builtins.open", m): + renovaveis = Renovaveis.read(ARQ_TESTE) + df = renovaveis.pee_pot_inst_per(df=True) + assert len(df) == 8 + assert df.at[0, "codigo_pee"] == 1 + assert df.at[0, "estagio_inicial"] == 1 + assert df.at[0, "estagio_final"] == 3 + assert df.at[0, "potencia_instalada"] == 99999.0 + + +def test_registro_renovaveis_pee_pot_inst_per(): + m: MagicMock = mock_open(read_data="".join(MockPEEPotenciaInstaladaPeriodo)) + r = PEEPotenciaInstaladaPeriodo() + with patch("builtins.open", m): + with open("", "") as fp: + r.read(fp) + + assert r.data == [1, 1, 3, 99999.0] + assert r.codigo_pee == 1 + r.codigo_pee = 0 + assert r.codigo_pee == 0 + assert r.estagio_inicial == 1 + r.estagio_inicial = 0 + assert r.estagio_inicial == 0 + assert r.estagio_final == 3 + r.estagio_final = 0 + assert r.estagio_final == 0 + assert r.potencia_instalada == 99999.0 + r.potencia_instalada = 0.0 + assert r.potencia_instalada == 0.0 + + +def test_df_renovaveis_pee_ger_per_pat_cen(): + m: MagicMock = mock_open(read_data="".join(MockRenovaveis)) + with patch("builtins.open", m): + renovaveis = Renovaveis.read(ARQ_TESTE) + df = renovaveis.pee_ger_per_pat_cen(df=True) + assert len(df) == 6 + assert df.at[0, "codigo_pee"] == 1 + assert df.at[0, "estagio_inicial"] == 1 + assert df.at[0, "estagio_final"] == 1 + assert df.at[0, "patamar"] == 1 + assert df.at[0, "cenario"] == 1 + assert df.at[0, "geracao"] == 81.0966 + assert df.at[5, "codigo_pee"] == 14 + assert df.at[5, "estagio_inicial"] == 3 + assert df.at[5, "estagio_final"] == 3 + assert df.at[5, "patamar"] == 3 + assert df.at[5, "cenario"] == 353 + assert df.at[5, "geracao"] == 1.611293 + + +def test_registro_renovaveis_pee_ger_per_pat_cen(): + m: MagicMock = mock_open( + read_data="".join(MockPEEGeracaoPeriodoPatamarCenario) + ) + r = PEEGeracaoPeriodoPatamarCenario() + with patch("builtins.open", m): + with open("", "") as fp: + r.read(fp) + + assert r.data == [1, 1, 1, 1, 1, 81.0966] + assert r.codigo_pee == 1 + r.codigo_pee = 0 + assert r.codigo_pee == 0 + assert r.estagio_inicial == 1 + r.estagio_inicial = 0 + assert r.estagio_inicial == 0 + assert r.estagio_final == 1 + r.estagio_final = 0 + assert r.estagio_final == 0 + assert r.patamar == 1 + r.patamar = 0 + assert r.patamar == 0 + assert r.cenario == 1 + r.cenario = 0 + assert r.cenario == 0 + assert r.geracao == 81.0966 + r.geracao = 0.0 + assert r.geracao == 0.0 + + +def test_eq_renovaveis(): + m: MagicMock = mock_open(read_data="".join(MockRenovaveis)) + with patch("builtins.open", m): + renovaveis1 = Renovaveis.read(ARQ_TESTE) + renovaveis2 = Renovaveis.read(ARQ_TESTE) + assert renovaveis1 == renovaveis2 + + +def test_neq_renovaveis(): + m: MagicMock = mock_open(read_data="".join(MockRenovaveis)) + with patch("builtins.open", m): + renovaveis1 = Renovaveis.read(ARQ_TESTE) + renovaveis2 = Renovaveis.read(ARQ_TESTE) + renovaveis1.pee_cad()[0].codigo_pee = -1 + assert renovaveis1 != renovaveis2 diff --git a/tests/mocks/arquivos/mlt.dat b/tests/mocks/arquivos/mlt.dat new file mode 100644 index 0000000..03c57f6 Binary files /dev/null and b/tests/mocks/arquivos/mlt.dat differ diff --git a/tests/mocks/arquivos/renovaveis.py b/tests/mocks/arquivos/renovaveis.py new file mode 100644 index 0000000..43e28fa --- /dev/null +++ b/tests/mocks/arquivos/renovaveis.py @@ -0,0 +1,57 @@ +MockRenovaveis = [ + "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&\n", + "& Cadastro dos parques eolicos equivalentes\n", + "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&\n", + " PEE-CAD; 1;Eolica SECO\n", + " PEE-CAD; 2;Eolica S\n", + " PEE-CAD; 3;Eolica NE\n", + " PEE-CAD; 4;Eolica N\n", + " PEE-CAD; 11;Fotovoltaica SECO\n", + " PEE-CAD; 12;Fotovoltaica S\n", + " PEE-CAD; 13;Fotovoltaica NE\n", + " PEE-CAD; 14;Fotovoltaica N\n", + " PEE-SUBM; 1; 1\n", + " PEE-SUBM; 2; 2\n", + " PEE-SUBM; 3; 3\n", + " PEE-SUBM; 4; 4\n", + " PEE-SUBM; 11; 1\n", + " PEE-SUBM; 12; 2\n", + " PEE-SUBM; 13; 3\n", + " PEE-SUBM; 14; 4\n", + " PEE-CONFIG-PER; 1; 1; 3;centralizado\n", + " PEE-CONFIG-PER; 2; 1; 3;centralizado\n", + " PEE-CONFIG-PER; 3; 1; 3;centralizado\n", + " PEE-CONFIG-PER; 4; 1; 3;centralizado\n", + " PEE-CONFIG-PER; 11; 1; 3;centralizado\n", + " PEE-CONFIG-PER; 12; 1; 3;centralizado\n", + " PEE-CONFIG-PER; 13; 1; 3;centralizado\n", + " PEE-CONFIG-PER; 14; 1; 3;centralizado\n", + " PEE-POT-INST-PER; 1; 1; 3; 99999\n", + " PEE-POT-INST-PER; 2; 1; 3; 99999\n", + " PEE-POT-INST-PER; 3; 1; 3; 99999\n", + " PEE-POT-INST-PER; 4; 1; 3; 99999\n", + " PEE-POT-INST-PER; 11; 1; 3; 99999\n", + " PEE-POT-INST-PER; 12; 1; 3; 99999\n", + " PEE-POT-INST-PER; 13; 1; 3; 99999\n", + " PEE-POT-INST-PER; 14; 1; 3; 99999\n", + "PEE-GER-PER-PAT-CEN;1;1;1;1;1;81.0966\n", + "PEE-GER-PER-PAT-CEN;1;1;1;2;1;87.6335\n", + "PEE-GER-PER-PAT-CEN;1;1;1;3;1;134.5896\n", + "PEE-GER-PER-PAT-CEN;1;2;2;1;1;102.0333\n", + "PEE-GER-PER-PAT-CEN;14;3;3;2;353;1.627114\n", + "PEE-GER-PER-PAT-CEN;14;3;3;3;353;1.611293\n", +] + +MockPEECadastro = " PEE-CAD; 1;Eolica SECO" + +MockPEESubmercado = " PEE-SUBM; 1; 1" + +MockPEEConfiguracaoPeriodo = ( + " PEE-CONFIG-PER; 1; 1; 3;centralizado" +) + +MockPEEPotenciaInstaladaPeriodo = ( + " PEE-POT-INST-PER; 1; 1; 3; 99999" +) + +MockPEEGeracaoPeriodoPatamarCenario = "PEE-GER-PER-PAT-CEN;1;1;1;1;1;81.0966"