diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c592d6..61543ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,26 @@ O formato segue o [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/). ## [Nao Publicado] +## [1.11.0] - 2026-06-09 + +### Adicionado + +- Suporte ao formato estendido do arquivo `hidr.dat`, com os coeficientes dos polinômios cota-volume e cota-área em precisão dupla (registros de 832 bytes), através do novo registro `RegistroUHEHidrF64`. A classe `Hidr` detecta o formato automaticamente pelo tamanho do arquivo, reconhecendo cadastros de 320 ou 600 usinas em registros de 792 ou 832 bytes, com possibilidade de forçar via `version="f32"` ou `version="f64"`. +- Propriedade `tamanho_registro` e método `converte_tamanho_registro` na classe `Hidr`, permitindo inspecionar e converter entre os formatos de precisão simples e dupla. + +## [1.10.0] - 2026-04-13 + +### Corrigido + +- Precisão decimal insuficiente em campos `FloatField` de diversos registros do `dadger`, que causava corrupção de valores na leitura e escrita (round-trip): + - Registro `PQ` (geração): `decimal_digits` de 0 para 4 com `compact=True`, eliminando 462 valores corrompidos, incluindo o zeramento completo de valores pequenos como `0.0563 → 0`. + - Registro `LU` (limites): `decimal_digits` de 1 para 2, corrigindo truncamentos como `860.55 → 860.5`. + - Registros `TX`, `VI`, `CI`, `CE`, `FP` e `AR`: `decimal_digits` de 0 para 3, por segurança, seguindo o padrão já adotado nos registros `FD` e `MP`. + +### Modificado + +- Dependência cfinterface atualizada para `>=1.10.0`, que disponibiliza o modo `compact` do `FloatField` (omissão do zero à esquerda para `|valor| < 1`, preservando a precisão em campos estreitos de 5 caracteres) e a correção da duplicação de registros no fallback de codificação da leitura. + ## [1.9.0] - 2026-03-10 ### Adicionado diff --git a/idecomp/__init__.py b/idecomp/__init__.py index 49b9317..aac56f7 100644 --- a/idecomp/__init__.py +++ b/idecomp/__init__.py @@ -6,6 +6,6 @@ e saída do DECOMP. """ -__version__ = "1.10.0" +__version__ = "1.11.0" from . import decomp # noqa diff --git a/idecomp/decomp/hidr.py b/idecomp/decomp/hidr.py index 7ad7e78..84878be 100644 --- a/idecomp/decomp/hidr.py +++ b/idecomp/decomp/hidr.py @@ -1,88 +1,215 @@ +from pathlib import Path from typing import IO, Any, TypeVar +from warnings import warn -import pandas as pd # type: ignore[import-untyped] +import pandas as pd # type: ignore[import-untyped] # no pandas-stubs package from cfinterface.files.registerfile import RegisterFile +from cfinterface.storage import StorageType from idecomp.config import MESES_ABREV -from idecomp.decomp.modelos.hidr import RegistroUHEHidr +from idecomp.decomp.modelos.hidr import RegistroUHEHidr, RegistroUHEHidrF64 class Hidr(RegisterFile): """ - Armazena os dados de entrada do NEWAVE referentes ao cadastro das + Armazena os dados de entrada do DECOMP referentes ao cadastro das usinas hidroelétricas. + + Suporta os dois formatos do arquivo binário: o formato com + registros de 792 bytes (coeficientes dos polinômios volume-cota e + cota-área em 32 bits) e o formato com registros de 832 bytes + (coeficientes em 64 bits). Na leitura, o formato é detectado + automaticamente pelo tamanho do arquivo, que corresponde a 320 ou + 600 registros em um dos dois tamanhos, podendo ser forçado com + `version="f32"` ou `version="f64"`. A escrita é feita sempre no + mesmo formato lido, a menos que seja feita uma conversão explícita + com :meth:`converte_tamanho_registro`. """ T = TypeVar("T") REGISTERS = [RegistroUHEHidr] - STORAGE = "BINARY" + VERSIONS = { + "f32": [RegistroUHEHidr], + "f64": [RegistroUHEHidrF64], + } + STORAGE = StorageType.BINARY + + __REGISTROS_POR_TAMANHO: dict[int, type[RegistroUHEHidr]] = { + RegistroUHEHidr.TAMANHO_REGISTRO: RegistroUHEHidr, + RegistroUHEHidrF64.TAMANHO_REGISTRO: RegistroUHEHidrF64, + } + + # Quantidades de registros (usinas) que o cadastro do hidr pode + # conter. O arquivo é um vetor de tamanho fixo, idêntico nos dois + # formatos de precisão. + __NUMEROS_REGISTROS_SUPORTADOS = (320, 600) + + # Tamanhos de arquivo (em bytes) reconhecidos, mapeando cada tamanho + # válido para a respectiva versão. Derivado das combinações de + # número de registros e tamanho de registro; as quatro combinações + # geram tamanhos distintos, sem ambiguidade entre os formatos. + __TAMANHOS_VALIDOS: dict[int, str] = { + num * tamanho: versao + for num in __NUMEROS_REGISTROS_SUPORTADOS + for versao, tamanho in ( + ("f32", RegistroUHEHidr.TAMANHO_REGISTRO), + ("f64", RegistroUHEHidrF64.TAMANHO_REGISTRO), + ) + } def __init__(self, data: Any = ...) -> None: super().__init__(data) self.__df: pd.DataFrame | None = None + @classmethod + def __detecta_versao(cls, content: str | bytes) -> str | None: + """ + Detecta o formato do arquivo a partir do tamanho do seu + conteúdo. Um arquivo hidr válido contém 320 ou 600 registros, + de 792 bytes (coeficientes em 32 bits) ou de 832 bytes + (coeficientes em 64 bits), resultando em um dos tamanhos + reconhecidos. Tamanhos diferentes geram um aviso e a leitura + assume o formato de 792 bytes. + """ + if isinstance(content, bytes): + num_bytes = len(content) + elif isinstance(content, str) and Path(content).is_file(): + num_bytes = Path(content).stat().st_size + else: + return None + versao = cls.__TAMANHOS_VALIDOS.get(num_bytes) + if versao is not None: + return versao + warn( + f"O tamanho do arquivo hidr ({num_bytes} bytes) não" + + " corresponde a nenhum formato conhecido (320 ou 600" + + " registros de 792 ou 832 bytes). Assumindo registros de" + + f" {RegistroUHEHidr.TAMANHO_REGISTRO} bytes. Use o" + + ' argumento version="f32" ou version="f64" para forçar o' + + " formato.", + stacklevel=3, + ) + return None + + @classmethod + def read( + cls, + content: str | bytes, + *args: Any, + version: str | None = None, + **kwargs: Any, + ) -> "Hidr": + if version is None: + version = cls.__detecta_versao(content) + h = super().read(content, *args, version=version, **kwargs) + assert isinstance(h, Hidr) + return h + + @property + def tamanho_registro(self) -> int: + """ + O tamanho em bytes dos registros do arquivo: 792 para o + formato com coeficientes dos polinômios em 32 bits ou 832 + para o formato em 64 bits. + + :return: O tamanho do registro em bytes + :rtype: int + """ + for r in self.data.of_type(RegistroUHEHidr): + return type(r).TAMANHO_REGISTRO + return RegistroUHEHidr.TAMANHO_REGISTRO + + def converte_tamanho_registro(self, precisao: str) -> None: + """ + Converte os registros do arquivo para o formato com o tamanho + de registro dado, de modo que a próxima escrita seja feita + neste formato. A conversão de 832 para 792 bytes implica em + perda de precisão nos coeficientes dos polinômios volume-cota + e cota-área. + + :param precisao: A precisão dos coeficients dos polinômios + ('f32' ou 'f64') + """ + + mapa_tamanhos = {"f32": 792, "f64": 832} + + tamanho_registro = mapa_tamanhos.get(precisao) + if tamanho_registro is None: + raise ValueError( + f"Precisão inválida: '{precisao}'. Os valores" + + " suportados são 'f32' e 'f64'." + ) + classe_registro = self.__class__.__REGISTROS_POR_TAMANHO[ + tamanho_registro + ] + for r in list(self.data.of_type(RegistroUHEHidr)): + if type(r) is classe_registro: + continue + novo = classe_registro(data=list(r.data)) + self.data.add_after(r, novo) + self.data.remove(r) + def write(self, to: str | IO[Any], *args: Any, **kwargs: Any) -> None: self.__atualiza_registros() super().write(to, *args, **kwargs) def __monta_df_de_registros(self) -> pd.DataFrame | None: - registros = self.data.get_registers_of_type(RegistroUHEHidr) - if registros is None: + registros: list[RegistroUHEHidr] = [ + r for r in self.data.of_type(RegistroUHEHidr) + ] + if len(registros) == 0: return None - elif isinstance(registros, RegistroUHEHidr): - registros = [registros] - df = pd.DataFrame( - columns=[ - "nome_usina", - "posto", - "submercado", - "empresa", - "codigo_usina_jusante", - "desvio", - "volume_minimo", - "volume_maximo", - "volume_vertedouro", - "volume_desvio", - "cota_minima", - "cota_maxima", - *[f"a{i}_volume_cota" for i in range(5)], - *[f"a{i}_cota_area" for i in range(5)], - *[f"evaporacao_{m}" for m in MESES_ABREV], - "numero_conjuntos_maquinas", - *[f"maquinas_conjunto_{i}" for i in range(1, 6)], - *[f"potencia_nominal_conjunto_{i}" for i in range(1, 6)], - *[f"queda_nominal_conjunto_{i}" for i in range(1, 6)], - *[f"vazao_nominal_conjunto_{i}" for i in range(1, 6)], - "produtibilidade_especifica", - "perdas", - "numero_polinomios_jusante", - *[f"a{i}_jusante_1" for i in range(5)], - *[f"a{i}_jusante_2" for i in range(5)], - *[f"a{i}_jusante_3" for i in range(5)], - *[f"a{i}_jusante_4" for i in range(5)], - *[f"a{i}_jusante_5" for i in range(5)], - *[f"a{i}_jusante_6" for i in range(5)], - *[f"referencia_jusante_{i}" for i in range(1, 7)], - "canal_fuga_medio", - "influencia_vertimento_canal_fuga", - "fator_carga_maximo", - "fator_carga_minimo", - "vazao_minima_historica", - "numero_unidades_base", - "tipo_turbina", - "representacao_conjunto", - "teif", - "ip", - "tipo_perda", - "data", - "observacao", - "volume_referencia", - "tipo_regulacao", - ] - ) - for i, r in enumerate(registros): - df.loc[i + 1] = [ + colunas = [ + "nome_usina", + "posto", + "submercado", + "empresa", + "codigo_usina_jusante", + "desvio", + "volume_minimo", + "volume_maximo", + "volume_vertedouro", + "volume_desvio", + "cota_minima", + "cota_maxima", + *[f"a{i}_volume_cota" for i in range(5)], + *[f"a{i}_cota_area" for i in range(5)], + *[f"evaporacao_{m}" for m in MESES_ABREV], + "numero_conjuntos_maquinas", + *[f"maquinas_conjunto_{i}" for i in range(1, 6)], + *[f"potencia_nominal_conjunto_{i}" for i in range(1, 6)], + *[f"queda_nominal_conjunto_{i}" for i in range(1, 6)], + *[f"vazao_nominal_conjunto_{i}" for i in range(1, 6)], + "produtibilidade_especifica", + "perdas", + "numero_polinomios_jusante", + *[f"a{i}_jusante_1" for i in range(5)], + *[f"a{i}_jusante_2" for i in range(5)], + *[f"a{i}_jusante_3" for i in range(5)], + *[f"a{i}_jusante_4" for i in range(5)], + *[f"a{i}_jusante_5" for i in range(5)], + *[f"a{i}_jusante_6" for i in range(5)], + *[f"referencia_jusante_{i}" for i in range(1, 7)], + "canal_fuga_medio", + "influencia_vertimento_canal_fuga", + "fator_carga_maximo", + "fator_carga_minimo", + "vazao_minima_historica", + "numero_unidades_base", + "tipo_turbina", + "representacao_conjunto", + "teif", + "ip", + "tipo_perda", + "data", + "observacao", + "volume_referencia", + "tipo_regulacao", + ] + + dados = [ + [ r.nome, r.posto, r.subsistema, @@ -123,16 +250,24 @@ def __monta_df_de_registros(self) -> pd.DataFrame | None: r.volume_referencia, r.tipo_regulacao, ] + for r in registros + ] + + df = pd.DataFrame( + data=dados, + index=range(1, len(registros) + 1), + columns=colunas, + ) df.index.name = "codigo_usina" df = df.astype( { "nome_usina": str, - "posto": int, - "submercado": int, - "empresa": int, - "codigo_usina_jusante": int, - "desvio": int, + "posto": "Int64", + "submercado": "Int64", + "empresa": "Int64", + "codigo_usina_jusante": "Int64", + "desvio": "Int64", "volume_minimo": float, "volume_maximo": float, "volume_vertedouro": float, @@ -142,16 +277,16 @@ def __monta_df_de_registros(self) -> pd.DataFrame | None: "produtibilidade_especifica": float, "perdas": float, "canal_fuga_medio": float, - "influencia_vertimento_canal_fuga": int, + "influencia_vertimento_canal_fuga": "Int64", "fator_carga_maximo": float, "fator_carga_minimo": float, - "vazao_minima_historica": int, - "numero_unidades_base": int, - "tipo_turbina": int, - "representacao_conjunto": int, + "vazao_minima_historica": "Int64", + "numero_unidades_base": "Int64", + "tipo_turbina": "Int64", + "representacao_conjunto": "Int64", "teif": float, "ip": float, - "tipo_perda": int, + "tipo_perda": "Int64", "data": str, "observacao": str, "volume_referencia": float, diff --git a/idecomp/decomp/modelos/hidr.py b/idecomp/decomp/modelos/hidr.py index 9f55617..58c31fc 100644 --- a/idecomp/decomp/modelos/hidr.py +++ b/idecomp/decomp/modelos/hidr.py @@ -140,6 +140,8 @@ class RegistroUHEHidr(Register): storage="BINARY", ) + TAMANHO_REGISTRO = 792 + @property def nome(self) -> str: return self.data[0] @@ -451,3 +453,143 @@ def tipo_regulacao(self) -> str: @tipo_regulacao.setter def tipo_regulacao(self, v: str) -> None: self.data[110] = v + + +class RegistroUHEHidrF64(RegistroUHEHidr): + """ + Registro com os dados associados a uma UHE, existentes no + arquivo HIDR, no formato em que os coeficientes dos polinômios + volume-cota e cota-área são armazenados em ponto flutuante + de 64 bits (registros de 832 bytes). + """ + + __slots__: list[str] = [] + + TAMANHO_REGISTRO = 832 + + LINE = Line( + [ + LiteralField(size=12, starting_position=0), # Nome + IntegerField(size=4, starting_position=12), # Posto + IntegerField(size=8, starting_position=16), # Posto BDH + IntegerField(size=4, starting_position=24), # Subsistema + IntegerField(size=4, starting_position=28), # Empresa + IntegerField(size=4, starting_position=32), # Jusante + IntegerField(size=4, starting_position=36), # Desvio + FloatField(size=4, starting_position=40), # Volume Mínimo + FloatField(size=4, starting_position=44), # Volume Máximo + FloatField(size=4, starting_position=48), # Volume Vertedouro + FloatField(size=4, starting_position=52), # Volume Desvio + FloatField(size=4, starting_position=56), # Cota Mínima + FloatField(size=4, starting_position=60), # Cota Máxima + FloatField(size=8, starting_position=64), # Pol VC 0 + FloatField(size=8, starting_position=72), # Pol VC 1 + FloatField(size=8, starting_position=80), # Pol VC 2 + FloatField(size=8, starting_position=88), # Pol VC 3 + FloatField(size=8, starting_position=96), # Pol VC 4 + FloatField(size=8, starting_position=104), # Pol CA 0 + FloatField(size=8, starting_position=112), # Pol CA 1 + FloatField(size=8, starting_position=120), # Pol CA 2 + FloatField(size=8, starting_position=128), # Pol CA 3 + FloatField(size=8, starting_position=136), # Pol CA 4 + IntegerField(size=4, starting_position=144), # Evap JAN + IntegerField(size=4, starting_position=148), # Evap FEV + IntegerField(size=4, starting_position=152), # Evap MAR + IntegerField(size=4, starting_position=156), # Evap ABR + IntegerField(size=4, starting_position=160), # Evap MAI + IntegerField(size=4, starting_position=164), # Evap JUN + IntegerField(size=4, starting_position=168), # Evap JUL + IntegerField(size=4, starting_position=172), # Evap AGO + IntegerField(size=4, starting_position=176), # Evap SET + IntegerField(size=4, starting_position=180), # Evap OUT + IntegerField(size=4, starting_position=184), # Evap NOV + IntegerField(size=4, starting_position=188), # Evap DEZ + IntegerField(size=4, starting_position=192), # Num. Conj. Máquinas + IntegerField( + size=4, starting_position=196 + ), # Num. Máquinas Conj. 1 + IntegerField( + size=4, starting_position=200 + ), # Num. Máquinas Conj. 2 + IntegerField( + size=4, starting_position=204 + ), # Num. Máquinas Conj. 3 + IntegerField( + size=4, starting_position=208 + ), # Num. Máquinas Conj. 4 + IntegerField( + size=4, starting_position=212 + ), # Num. Máquinas Conj. 5 + FloatField(size=4, starting_position=216), # Potência Conj. 1 + FloatField(size=4, starting_position=220), # Potência Conj. 2 + FloatField(size=4, starting_position=224), # Potência Conj. 3 + FloatField(size=4, starting_position=228), # Potência Conj. 4 + FloatField(size=4, starting_position=232), # Potência Conj. 5 + LiteralField(size=300, starting_position=236), # Campos ignorados + FloatField(size=4, starting_position=536), # H Nominal 1 + FloatField(size=4, starting_position=540), # H Nominal 2 + FloatField(size=4, starting_position=544), # H Nominal 3 + FloatField(size=4, starting_position=548), # H Nominal 4 + FloatField(size=4, starting_position=552), # H Nominal 5 + IntegerField(size=4, starting_position=556), # Q Nominal 1 + IntegerField(size=4, starting_position=560), # Q Nominal 2 + IntegerField(size=4, starting_position=564), # Q Nominal 3 + IntegerField(size=4, starting_position=568), # Q Nominal 4 + IntegerField(size=4, starting_position=572), # Q Nominal 5 + FloatField(size=4, starting_position=576), # Produtibilidade + FloatField(size=4, starting_position=580), # Perdas + IntegerField(size=4, starting_position=584), # Núm. Pol. Jus. + FloatField(size=4, starting_position=588), # Pol. Jus. 1 - 0 + FloatField(size=4, starting_position=592), # Pol. Jus. 1 - 1 + FloatField(size=4, starting_position=596), # Pol. Jus. 1 - 2 + FloatField(size=4, starting_position=600), # Pol. Jus. 1 - 3 + FloatField(size=4, starting_position=604), # Pol. Jus. 1 - 4 + FloatField(size=4, starting_position=608), # Pol. Jus. 2 - 0 + FloatField(size=4, starting_position=612), # Pol. Jus. 2 - 1 + FloatField(size=4, starting_position=616), # Pol. Jus. 2 - 2 + FloatField(size=4, starting_position=620), # Pol. Jus. 2 - 3 + FloatField(size=4, starting_position=624), # Pol. Jus. 2 - 4 + FloatField(size=4, starting_position=628), # Pol. Jus. 3 - 0 + FloatField(size=4, starting_position=632), # Pol. Jus. 3 - 1 + FloatField(size=4, starting_position=636), # Pol. Jus. 3 - 2 + FloatField(size=4, starting_position=640), # Pol. Jus. 3 - 3 + FloatField(size=4, starting_position=644), # Pol. Jus. 3 - 4 + FloatField(size=4, starting_position=648), # Pol. Jus. 4 - 0 + FloatField(size=4, starting_position=652), # Pol. Jus. 4 - 1 + FloatField(size=4, starting_position=656), # Pol. Jus. 4 - 2 + FloatField(size=4, starting_position=660), # Pol. Jus. 4 - 3 + FloatField(size=4, starting_position=664), # Pol. Jus. 4 - 4 + FloatField(size=4, starting_position=668), # Pol. Jus. 5 - 0 + FloatField(size=4, starting_position=672), # Pol. Jus. 5 - 1 + FloatField(size=4, starting_position=676), # Pol. Jus. 5 - 2 + FloatField(size=4, starting_position=680), # Pol. Jus. 5 - 3 + FloatField(size=4, starting_position=684), # Pol. Jus. 5 - 4 + FloatField(size=4, starting_position=688), # Pol. Jus. 6 - 0 + FloatField(size=4, starting_position=692), # Pol. Jus. 6 - 1 + FloatField(size=4, starting_position=696), # Pol. Jus. 6 - 2 + FloatField(size=4, starting_position=700), # Pol. Jus. 6 - 3 + FloatField(size=4, starting_position=704), # Pol. Jus. 6 - 4 + FloatField(size=4, starting_position=708), # Pol. Jus. REF - 1 + FloatField(size=4, starting_position=712), # Pol. Jus. REF - 2 + FloatField(size=4, starting_position=716), # Pol. Jus. REF - 3 + FloatField(size=4, starting_position=720), # Pol. Jus. REF - 4 + FloatField(size=4, starting_position=724), # Pol. Jus. REF - 5 + FloatField(size=4, starting_position=728), # Pol. Jus. REF - 6 + FloatField(size=4, starting_position=732), # Canal Fuga Médio + IntegerField(size=4, starting_position=736), # Influência no vert + FloatField(size=4, starting_position=740), # Fator de Carga Máximo + FloatField(size=4, starting_position=744), # Fator de Carga Mínimo + IntegerField(size=4, starting_position=748), # Vazão Mínima Hist. + IntegerField(size=4, starting_position=752), # Núm. de Unid. Base + IntegerField(size=4, starting_position=756), # Tipo de Turbina + IntegerField(size=4, starting_position=760), # Repr. Conjunto + FloatField(size=4, starting_position=764), # TEIF + FloatField(size=4, starting_position=768), # IP + IntegerField(size=4, starting_position=772), # Tipo Perda + LiteralField(size=12, starting_position=776), # Data + LiteralField(size=39, starting_position=788), # Observação + FloatField(size=4, starting_position=827), # Volume de Referência + LiteralField(size=1, starting_position=831), # Tipo de Regular. + ], + storage="BINARY", + ) diff --git a/tests/decomp/test_hidr.py b/tests/decomp/test_hidr.py index 27391e3..c7082c9 100644 --- a/tests/decomp/test_hidr.py +++ b/tests/decomp/test_hidr.py @@ -1,56 +1,232 @@ -from idecomp.decomp.modelos.hidr import RegistroUHEHidr -from idecomp.decomp.hidr import Hidr +from io import BytesIO +from unittest.mock import MagicMock, patch + +import pandas as pd # type: ignore[import-untyped] +import pytest -from os.path import join +from idecomp.decomp.hidr import Hidr +from idecomp.decomp.modelos.hidr import RegistroUHEHidr, RegistroUHEHidrF64 from tests.mocks.mock_open import mock_open -from unittest.mock import MagicMock, patch +ARQ_TESTE = "./tests/mocks/arquivos/hidr.dat" +ARQ_TESTE_F64 = "./tests/mocks/arquivos/hidr_f64.dat" -ARQ_TEST = "./tests/mocks/arquivos/hidr.dat" +# Os dois mocks são arquivos reais independentes, com quantidades de +# usinas diferentes: o mock f32 (hidr.dat) tem 600 registros e o mock +# f64 (hidr_f64.dat) tem 320. +NUM_REGISTROS_F32 = 600 +NUM_REGISTROS_F64 = 320 + + +def converte_arq_teste_para_f64() -> bytes: + h = Hidr.read(ARQ_TESTE) + h.converte_tamanho_registro("f64") + buffer = BytesIO() + h.write(buffer) + return buffer.getvalue() def test_registro_uhe_hidr(): r = RegistroUHEHidr() - with open(join(ARQ_TEST), "rb") as fp: + with open(ARQ_TESTE, "rb") as fp: r.read(fp, storage="BINARY") assert len(r.data) == 111 assert r.nome == "CAMARGOS" +def test_registro_uhe_hidr_f64(): + r = RegistroUHEHidrF64() + with open(ARQ_TESTE_F64, "rb") as fp: + r.read(fp, storage="BINARY") + + assert len(r.data) == 111 + assert r.nome == "CAMARGOS" + assert r.posto == 1 + assert r.volume_minimo == pytest.approx(107.6, rel=1e-6) + assert r.polinomio_volume_cota == pytest.approx( + [893.0279, 0.06682515, -0.0001183834, 1.282643e-07, -5.560146e-11], + rel=1e-12, + ) + assert r.polinomio_cota_area == pytest.approx( + [3211416.0, -10547.2, 11.54139, -0.004207735, 0.0], + rel=1e-12, + abs=1e-15, + ) + assert r.canal_fuga_medio == pytest.approx(885.72906, rel=1e-6) + assert r.data_referencia == "13-05-26" + assert r.tipo_regulacao == "M" + + def test_atributos_encontrados_hidr(): - h = Hidr.read(ARQ_TEST) + h = Hidr.read(ARQ_TESTE) + assert h.cadastro is not None + + +def test_atributos_encontrados_hidr_f64(): + h = Hidr.read(ARQ_TESTE_F64) assert h.cadastro is not None def test_atributos_nao_encontrados_hidr(): - m: MagicMock = mock_open(read_data=b"") + m: MagicMock = mock_open(read_data="") + with patch("builtins.open", m): + ad = Hidr.read(ARQ_TESTE) + assert ad.cadastro is None + + +def test_atributos_nao_encontrados_hidr_f64(): + m: MagicMock = mock_open(read_data="") with patch("builtins.open", m): - ad = Hidr.read(ARQ_TEST) + ad = Hidr.read(ARQ_TESTE_F64) assert ad.cadastro is None def test_eq_hidr(): - h1 = Hidr.read(ARQ_TEST) - h2 = Hidr.read(ARQ_TEST) + h1 = Hidr.read(ARQ_TESTE) + h2 = Hidr.read(ARQ_TESTE) + assert h1 == h2 + + +def test_eq_hidr_f64(): + h1 = Hidr.read(ARQ_TESTE_F64) + h2 = Hidr.read(ARQ_TESTE_F64) assert h1 == h2 def test_neq_hidr(): - h1 = Hidr.read(ARQ_TEST) - h2 = Hidr.read(ARQ_TEST) + h1 = Hidr.read(ARQ_TESTE) + h2 = Hidr.read(ARQ_TESTE) h2.cadastro.iloc[0, 0] = "TESTE" - m: MagicMock = mock_open(read_data=b"") + m: MagicMock = mock_open(read_data="") with patch("builtins.open", m): - h2.write("") + h2.write(ARQ_TESTE) assert h1 != h2 +def test_neq_hidr_f64(): + h1 = Hidr.read(ARQ_TESTE_F64) + h2 = Hidr.read(ARQ_TESTE_F64) + h2.cadastro.iloc[0, 0] = "TESTE" + h2.write(BytesIO()) + assert h1 != h2 + + +def test_deteccao_formato_792(): + # Mock f32 com 600 registros + h = Hidr.read(ARQ_TESTE) + assert h.tamanho_registro == RegistroUHEHidr.TAMANHO_REGISTRO + assert len(h.cadastro) == NUM_REGISTROS_F32 + + +def test_deteccao_formato_832(): + # Mock f64 com 320 registros + h = Hidr.read(ARQ_TESTE_F64) + assert h.tamanho_registro == RegistroUHEHidrF64.TAMANHO_REGISTRO + assert len(h.cadastro) == NUM_REGISTROS_F64 + + +def test_deteccao_formato_f32_320(): + # Os dois tamanhos de registro podem conter 320 ou 600 usinas. O + # mock f32 tem 600 registros; truncamos para 320 para validar a + # detecção do tamanho f32 menor. + with open(ARQ_TESTE, "rb") as fp: + conteudo = fp.read(NUM_REGISTROS_F64 * RegistroUHEHidr.TAMANHO_REGISTRO) + h = Hidr.read(conteudo) + assert h.tamanho_registro == RegistroUHEHidr.TAMANHO_REGISTRO + assert len(h.cadastro) == NUM_REGISTROS_F64 + + +def test_deteccao_formato_f64_600(): + # Converte o mock f32 (600 registros) para f64, obtendo um conteúdo + # de 600 registros de 832 bytes, e valida a detecção desse formato. + h32 = Hidr.read(ARQ_TESTE) + h32.converte_tamanho_registro("f64") + buffer = BytesIO() + h32.write(buffer) + h = Hidr.read(buffer.getvalue()) + assert h.tamanho_registro == RegistroUHEHidrF64.TAMANHO_REGISTRO + assert len(h.cadastro) == NUM_REGISTROS_F32 + + +def test_leitura_com_versao_explicita(): + h32 = Hidr.read(ARQ_TESTE, version="f32") + assert h32.tamanho_registro == RegistroUHEHidr.TAMANHO_REGISTRO + h64 = Hidr.read(ARQ_TESTE_F64, version="f64") + assert h64.tamanho_registro == RegistroUHEHidrF64.TAMANHO_REGISTRO + + +def test_escrita_mantem_formato_f64(): + h64 = Hidr.read(ARQ_TESTE_F64) + buffer = BytesIO() + h64.write(buffer) + assert ( + len(buffer.getvalue()) + == NUM_REGISTROS_F64 * RegistroUHEHidrF64.TAMANHO_REGISTRO + ) + + +def test_conversao_para_f64(): + conteudo = converte_arq_teste_para_f64() + assert ( + len(conteudo) == NUM_REGISTROS_F32 * RegistroUHEHidrF64.TAMANHO_REGISTRO + ) + h32 = Hidr.read(ARQ_TESTE) + h64 = Hidr.read(conteudo) + assert h64.tamanho_registro == RegistroUHEHidrF64.TAMANHO_REGISTRO + pd.testing.assert_frame_equal(h32.cadastro, h64.cadastro) + + +def test_conversao_para_f32(): + h64 = Hidr.read(ARQ_TESTE_F64) + h32 = Hidr.read(ARQ_TESTE_F64) + h32.converte_tamanho_registro("f32") + buffer = BytesIO() + h32.write(buffer) + assert ( + len(buffer.getvalue()) + == NUM_REGISTROS_F64 * RegistroUHEHidr.TAMANHO_REGISTRO + ) + relido = Hidr.read(buffer.getvalue()) + assert relido.tamanho_registro == RegistroUHEHidr.TAMANHO_REGISTRO + # A conversão para f32 perde precisão nos coeficientes dos + # polinômios, então a comparação é aproximada + pd.testing.assert_frame_equal( + h64.cadastro, relido.cadastro, check_exact=False, rtol=1e-6 + ) + + +def test_conversao_ida_e_volta_preserva_valores(): + h1 = Hidr.read(ARQ_TESTE) + h2 = Hidr.read(ARQ_TESTE) + h2.converte_tamanho_registro("f64") + h2.converte_tamanho_registro("f32") + assert h2.tamanho_registro == RegistroUHEHidr.TAMANHO_REGISTRO + assert h1 == h2 + + +def test_conversao_precisao_invalida(): + h = Hidr.read(ARQ_TESTE) + with pytest.raises(ValueError, match="Precisão inválida: 'f16'"): + h.converte_tamanho_registro("f16") + + +def test_deteccao_formato_desconhecido(): + # 82368 bytes não corresponde a 320 nem a 600 registros de 792 ou + # 832 bytes, então o tamanho não é reconhecido: a leitura emite um + # aviso e assume o formato f32 (792 bytes). + with open(ARQ_TESTE, "rb") as fp: + conteudo = fp.read(82368) + with pytest.warns(UserWarning, match="não corresponde a nenhum formato"): + h = Hidr.read(conteudo) + assert h.tamanho_registro == RegistroUHEHidr.TAMANHO_REGISTRO + + def test_leitura_escrita_hidr(): - h1 = Hidr.read(ARQ_TEST) - m_escrita: MagicMock = mock_open(read_data=b"") + h1 = Hidr.read(ARQ_TESTE) + m_escrita: MagicMock = mock_open(read_data="") with patch("builtins.open", m_escrita): - h1.write("") + h1.write(ARQ_TESTE) # Recupera o que foi escrito chamadas = m_escrita.mock_calls linhas_escritas = [ @@ -58,5 +234,14 @@ def test_leitura_escrita_hidr(): ] m_releitura: MagicMock = mock_open(read_data=b"".join(linhas_escritas)) with patch("builtins.open", m_releitura): - h2 = Hidr.read(ARQ_TEST) + h2 = Hidr.read(ARQ_TESTE) assert h1 == h2 + + +def test_leitura_escrita_hidr_f64(): + h1 = Hidr.read(ARQ_TESTE_F64) + buffer = BytesIO() + h1.write(buffer) + h2 = Hidr.read(buffer.getvalue()) + assert h2.tamanho_registro == RegistroUHEHidrF64.TAMANHO_REGISTRO + assert h1 == h2 diff --git a/tests/mocks/arquivos/hidr_f64.dat b/tests/mocks/arquivos/hidr_f64.dat new file mode 100644 index 0000000..52eb055 Binary files /dev/null and b/tests/mocks/arquivos/hidr_f64.dat differ