test(decisions): comparateur --sous-palier, seul ECO_FLOOR_GRID → ECO_FLOOR_BELOW_STAGE est admis

Retire le motif des niveaux « eco » portant l'un des deux codes et les
confronte deux à deux : tout autre passage, ou un ECO_FLOOR_GRID aux
paramètres modifiés, est un ÉCART. targetW, counts, progress du niveau restent
comparés. Les changements sont listés par scénario et par charge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-10-19 08:43:30 +02:00
parent 30527b9373
commit 35808f17bd

View File

@ -2,7 +2,8 @@
# Comparateur « IDENTIQUE HORS MOTIFS » (correctifs moteur a et b, etm70).
#
# etm-powersync-compare-hors-motifs.py <attendu.ndjson[.xz]> <obtenu.ndjson[.xz]> [--cas <sortie.md>]
# [--achat] [--strategie] [--lignes <sortie.txt>]
# [--achat] [--strategie] [--sous-palier]
# [--lignes <sortie.txt>]
#
# Un lot qui ne change QUE des motifs doit laisser tout le reste de la référence `decisions`
# intact à l'octet près. Ce script retire de chaque ligne, et SEULEMENT d'elle :
@ -15,6 +16,11 @@
# change cette VALEUR PUBLIÉE sans toucher aux consignes doit passer ce mode-là.
# Avec --strategie (brief 4 T6, décision du 2026-10-17), il ramène EN PLUS `plan.strategy`
# `rule-based` à `rules` d'une ligne de plan direct : le renommage du plan rule-based, et lui seul.
# Avec --sous-palier (lot éco sous palier, 2026-10-19), il retire EN PLUS le motif des niveaux
# « eco » portant ECO_FLOOR_GRID ou ECO_FLOOR_BELOW_STAGE, puis les confronte deux à deux : seul
# ECO_FLOOR_GRID → ECO_FLOOR_BELOW_STAGE est admis ; un ECO_FLOOR_GRID dont les paramètres
# changent, ou tout autre passage, est un ÉCART. Le reste du niveau (targetW, counts, progress)
# reste comparé.
# Tout le reste — consignes, powerW, estimatedPowerW, états, relais, courants, budget, draw,
# counts, niveaux et leurs motifs, avancement — doit être IDENTIQUE. Code 0 si c'est le cas,
# 1 sinon (et le premier écart est montré). La liste des motifs qui changent est imprimée, ou
@ -50,11 +56,23 @@ def sans_achat(niveau, charge, achats):
achats.append((charge, prog.pop('purchasedWh', ABSENT)))
def normaliser(ligne, achat=False, strategie=False):
"""Rend (ligne sans motifs, [(charge, motif retiré)], [(charge, purchasedWh retiré)])."""
ECO_SOUS_PALIER = ('ECO_FLOOR_GRID', 'ECO_FLOOR_BELOW_STAGE')
def sans_motif_eco(niveau, cle_motif, charge, motifs_eco):
motif = niveau.get(cle_motif)
if niveau.get('level') == 'eco' and isinstance(motif, dict) \
and motif.get('code') in ECO_SOUS_PALIER:
motifs_eco.append((charge, niveau.pop(cle_motif)))
def normaliser(ligne, achat=False, strategie=False, sous_palier=False):
"""Rend (ligne sans motifs, [(charge, motif retiré)], [(charge, purchasedWh retiré)],
[(charge, motif de niveau éco retiré)])."""
d = json.loads(ligne)
retires = []
achats = []
motifs_eco = []
if strategie and d.get('src') == 'plan' and (d.get('plan') or {}).get('strategy') == 'rule-based':
d['plan']['strategy'] = 'rules'
if d.get('src') == 'cycle':
@ -64,6 +82,8 @@ def normaliser(ligne, achat=False, strategie=False):
sans_manque(n, 'decision')
if achat:
sans_achat(n, ch.get('loadId'), achats)
if sous_palier:
sans_motif_eco(n, 'decision', ch.get('loadId'), motifs_eco)
elif d.get('src') == 'plan':
for s in (d.get('plan') or {}).get('timeSlots', []) or []:
for a in s.get('actions', []) or []:
@ -72,7 +92,9 @@ def normaliser(ligne, achat=False, strategie=False):
sans_manque(n, 'reason')
if achat:
sans_achat(n, a.get('loadId'), achats)
return json.dumps(d, sort_keys=True, ensure_ascii=False), retires, achats
if sous_palier:
sans_motif_eco(n, 'reason', a.get('loadId'), motifs_eco)
return json.dumps(d, sort_keys=True, ensure_ascii=False), retires, achats, motifs_eco
def achat_court(v):
@ -100,6 +122,7 @@ def main():
sortie_lignes = sys.argv[sys.argv.index('--lignes') + 1] if '--lignes' in sys.argv else None
achat = '--achat' in sys.argv
strategie = '--strategie' in sys.argv
sous_palier = '--sous-palier' in sys.argv
lignes_changees = []
if len(attendu) != len(obtenu):
@ -112,15 +135,25 @@ def main():
if a == b:
brutes_identiques += 1
continue
na, ra, aa = normaliser(a, achat, strategie)
nb, rb, ab = normaliser(b, achat, strategie)
na, ra, aa, ea = normaliser(a, achat, strategie, sous_palier)
nb, rb, ab, eb = normaliser(b, achat, strategie, sous_palier)
lignes_changees.append((i, a, b))
if na != nb:
if na != nb or len(ea) != len(eb):
print(f'ÉCART HORS MOTIFS{" ET HORS ACHAT" if achat else ""}'
f'{" ET HORS STRATÉGIE" if strategie else ""} à la ligne {i} :')
f'{" ET HORS STRATÉGIE" if strategie else ""}'
f'{" ET HORS MOTIF ÉCO SOUS PALIER" if sous_palier else ""} à la ligne {i} :')
print('- ' + a[:3000])
print('+ ' + b[:3000])
return 1
# Le motif d'un niveau éco : identique, ou ECO_FLOOR_GRID devenu ECO_FLOOR_BELOW_STAGE.
for (charge, ma), (_, mb) in zip(ea, eb):
if ma == mb:
continue
if (ma.get('code'), mb.get('code')) != ECO_SOUS_PALIER:
print(f'ÉCART DE MOTIF ÉCO à la ligne {i} ({charge}) : {ma} → {mb}')
return 1
cas[(json.loads(a).get('fn'), charge, f'niveau éco {ma["code"]}',
f'niveau éco {mb["code"]}')] += 1
fn = json.loads(a).get('fn')
for (charge, ma), (_, mb) in zip(ra, rb):
if ma != mb:
@ -134,10 +167,11 @@ def main():
if sa != sb:
cas[(fn, '(plan)', f'strategy {sa}', f'strategy {sb}')] += 1
# Le manque d'un niveau change sans que le motif de tête change : à dire aussi.
elif ra == rb and aa == ab:
elif ra == rb and aa == ab and ea == eb:
cas[(fn, '(niveau)', 'missingWh', 'missingWh corrigé')] += 1
hors = 'HORS MOTIFS' + (' ET HORS ACHAT' if achat else '') + (' ET HORS STRATÉGIE' if strategie else '')
hors = ('HORS MOTIFS' + (' ET HORS ACHAT' if achat else '') + (' ET HORS STRATÉGIE' if strategie else '')
+ (' ET HORS MOTIF ÉCO SOUS PALIER' if sous_palier else ''))
print(f'IDENTIQUE {hors} : {len(attendu)} lignes, dont {brutes_identiques} identiques '
f'à l\'octet et {len(attendu) - brutes_identiques} qui ne diffèrent que par '
f'{"les motifs" + (", purchasedWh" if achat else "") + (" ou plan.strategy" if strategie else "")}')