Skip to content

fix: layout do clast.dat e registro em branco no bloco de intercambio do sistema.dat - #123

Merged
rjmalves merged 5 commits into
rjmalves:mainfrom
dcpirex:fix/clast-layout-e-sistema-intercambio
Aug 2, 2026
Merged

fix: layout do clast.dat e registro em branco no bloco de intercambio do sistema.dat#123
rjmalves merged 5 commits into
rjmalves:mainfrom
dcpirex:fix/clast-layout-e-sistema-intercambio

Conversation

@dcpirex

@dcpirex dcpirex commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Três correções encontradas ao usar o inewave para montar um estudo de
extensão de horizonte (5 → 6 anos) sobre um deck de PMO. Cada uma está
em seu próprio commit e são independentes entre si.

1. clast.dat: campo tipo_combustivel invade a coluna de custo

O template do arquivo reserva 10 caracteres para TIPO COMB.:

 XXXX XXXXXXXXXXXX XXXXXXXXXX XXXX.XX XXXX.XX ...
                   ^--- 10 chars (colunas 20-29)

O modelo declarava LiteralField(12, 19), cobrindo os índices 19–30 e
invadindo os dois primeiros caracteres do primeiro campo de custo
(FloatField(8, 29)).

Efeito: usinas com CVU ≥ 1000 saem com o combustível corrompido.

codigo_usina=491  nome_usina="CAMPO GRANDE"
tipo_combustivel="Gas        1"   valor=1314.04

Num deck de PMO isso atingia 82 das 159 usinas. A escrita mascarava o
defeito — o campo de custo sobrescreve os bytes invadidos e o arquivo
sai idêntico —, então o dano ficava restrito ao DataFrame exposto ao
usuário: qualquer agregação por tipo de combustível gera categorias
fantasma.

2. clast.dat: número de colunas de custo fixo em 5

campos_custos usava range(5). O read() aceita
numero_anos_planejamento, mas a Line é montada no __init__ e
Clast.read() nunca repassa o argumento — na prática o valor era
sempre 5.

Decks com 4 colunas ganhavam uma coluna fantasma de NaN; decks com
horizonte estendido para 6 anos eram truncados na escrita.

A contagem passa a vir da linha de template (XXXX.XX), que já está no
cabeçalho do arquivo, com fallback para o comportamento anterior caso
ela esteja ausente. A escrita também ajusta o cabeçalho quando o
DataFrame ganha ou perde anos.

3. sistema.dat: registro em branco no bloco de intercâmbio

Manual do NEWAVE v30.0.2, seção 3.7, Bloco 3:

Os registros tipo 2 e 3 devem ser fornecidos agrupadamente e os grupos
serão separados por um registro em branco, de existência obrigatória.

O leitor já respeita isso. O escritor repetia o cabeçalho tipo 1 no
lugar do registro em branco. O round-trip dentro do inewave funciona
(o leitor aceita as duas formas), mas o arquivo gerado diverge do
formato documentado.

Testes

Toda a suíte continua passando (1170 testes). Foram adicionados:

  • test_tipo_combustivel_nao_invade_campo_custo
  • test_leitura_clast_4_colunas_custo / test_leitura_clast_6_colunas_custo
  • test_leitura_escrita_clast_4_colunas_custo (round-trip completo)
  • test_escrita_clast_ajusta_cabecalho_ao_estender_horizonte
  • test_escrita_intercambio_usa_registro_em_branco

Validação adicional contra um deck de PMO real: round-trip byte-a-byte
idêntico nas 159 usinas do bloco de UTEs do clast.dat e no bloco de
intercâmbio do sistema.dat.

Notas de escopo

Dois pontos correlatos ficaram de fora deste PR por não serem
correções isoladas — vou abrir uma issue para discuti-los:

  • No bloco de intercâmbio, a coluna sentido funde o flag do campo 3 do
    registro tipo 1 (0 = limite, 1 = intercâmbio mínimo obrigatório)
    com o sentido A→B / B→A, que pelo manual é posicional. Corrigir isso é
    breaking change no DataFrame público.
  • No bloco de modificação do clast.dat, as datas saem com mês
    zero-padded ("07 2026") onde o original usa espaço (" 7 2026"). O
    mesmo format="%m %Y" é usado em agrint.dat e expt.dat, e não há
    correção portável de uma linha (%_m é extensão do glibc), então
    prefiro discutir a abordagem antes.

Claude Code and others added 5 commits July 20, 2026 00:26
O template do arquivo reserva 10 caracteres para TIPO COMB. (colunas
20-29), mas o modelo declarava LiteralField(12, 19), invadindo os dois
primeiros caracteres do primeiro campo de custo (FloatField(8, 29)).

Usinas com CVU >= 1000 tinham o combustivel lido como 'Gas        1'
em vez de 'Gas'. No deck NW202701 isso afetava 82 das 159 usinas. A
escrita mascarava o defeito, pois o campo de custo sobrescrevia os
bytes invadidos - o dano era restrito ao DataFrame exposto ao usuario.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
O bloco de UTEs fixava 5 colunas de custo (range(5)) na construcao da
Line. O parametro numero_anos_planejamento existia no read(), mas a
Line ja havia sido montada no __init__ e Clast.read() nunca repassava
o argumento - na pratica o valor era sempre 5.

Decks com 4 colunas ganhavam uma coluna fantasma de NaN; decks com
horizonte estendido para 6 anos eram truncados silenciosamente na
escrita. O numero de colunas passa a ser detectado a partir da linha
de template (XXXX.XX), que ja esta no cabecalho do arquivo, com
fallback para o comportamento anterior caso ela esteja ausente.

A escrita tambem ajusta o cabecalho quando o DataFrame ganha ou perde
anos, para que a releitura nao trunque os dados de volta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
O teste anterior so conferia a contagem de XXXX.XX no cabecalho, que ja
era 4 antes da correcao - passava mesmo sem o fix. Passa a fazer o
round-trip completo (leitura, escrita, releitura) e exige que a
releitura mantenha indice_ano_estudo em [1,2,3,4], sem a 5a coluna
fantasma que o codigo antigo perpetuava.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
O manual do NEWAVE v30.0.2 (secao 3.7, Bloco 3) determina que os
grupos de registros tipo 2 (A->B) e tipo 3 (B->A) sejam 'separados por
um registro em branco, de existencia obrigatoria'.

O leitor ja respeitava isso - interpreta a linha em branco como virada
de grupo. O escritor, porem, repetia o cabecalho tipo 1 no lugar do
registro em branco. O round-trip dentro do inewave funcionava, porque o
leitor aceita as duas formas, mas o arquivo gerado divergia do formato
documentado.

O cabecalho tipo 1 passa a ser emitido apenas quando o par de
submercados muda; a virada de sentido emite a linha em branco.
Guard defensivo em test_tipo_combustivel_nao_invade_campo_custo: sem o
short-circuit, um tipo_combustivel vazio apos strip() levantaria
IndexError em c.strip()[-1]. Nao ocorre nos mocks atuais, mas evita
fragilidade caso um mock futuro inclua combustivel em branco.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.36842% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 91.10%. Comparing base (b9f931c) to head (2909ddb).

Files with missing lines Patch % Lines
inewave/newave/modelos/clast.py 96.87% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #123      +/-   ##
==========================================
+ Coverage   91.07%   91.10%   +0.02%     
==========================================
  Files         526      526              
  Lines       18944    18976      +32     
==========================================
+ Hits        17254    17288      +34     
+ Misses       1690     1688       -2     
Flag Coverage Δ
unittests 91.10% <97.36%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rjmalves
rjmalves merged commit 0b2041c into rjmalves:main Aug 2, 2026
9 of 10 checks passed
rjmalves added a commit that referenced this pull request Aug 2, 2026
- Fixa `ruff<0.16` no extra de lint. Como o uv.lock é gitignored, a CI
  resolvia o ruff para a release mais recente a cada execução; uma versão
  nova passou a reordenar imports em todo o repositório e quebrou o lint
  sem relação com a mudança em revisão.
- Adiciona a entrada de changelog das três correções do #123, creditando
  @dcpirex.
- Move as constantes de layout do clast (INICIO_CUSTOS/LARGURA_CUSTO) para
  antes do __init__, junto de FIM_BLOCO, seguindo a convenção do arquivo
  (neutro em comportamento).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants