fix(tools): trace-replay compare une action state sur son état seul (brief 4 T7)

Le plan du rule-based tracé porte la puissance ESTIMÉE d'une action state ; un optimiseur ne
la donne pas (§6 ne la définit pas). Le rejeu d'une trace rule-based contre l'optimiseur de
référence voyait donc une divergence sur chaque PAC. Seuls l'état (state) et la puissance
d'une consigne (setpoint) sont comparés. Test unitaire ajouté (7 / 0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-10-17 13:16:43 +02:00
parent 6fcce1a470
commit f42dc709e3
2 changed files with 37 additions and 3 deletions

View File

@ -228,5 +228,35 @@ class TestTraceReplay(unittest.TestCase):
self.assertNotIn('requêtes rejouées : 0', out) self.assertNotIn('requêtes rejouées : 0', out)
class TestComparaison(unittest.TestCase):
"""La comparaison seule (fonctions de l'outil, sans serveur)."""
@classmethod
def setUpClass(cls):
from importlib.machinery import SourceFileLoader
cls.outil = SourceFileLoader('trace_replay', OUTIL).load_module()
def test_action_state_comparee_sur_son_etat_seul(self):
# Brief 4 T7 : le plan du rule-based porte la puissance ESTIMÉE d'une action `state`,
# l'optimiseur de référence ne la donne pas (le §6 ne la définit pas) — ce n'est pas un écart.
o = self.outil
maintenant = o.instant('2026-06-08T13:00:00Z')
creneau = {'from': '2026-06-08T13:00:00Z', 'to': '2026-06-08T13:01:00Z'}
regles = {'result': {'slots': [dict(creneau, actions=[
{'loadId': 'pac', 'kind': 'state', 'state': 3, 'powerW': 1500},
{'loadId': 'ecs', 'kind': 'setpoint', 'powerW': 1000}])]}}
optimiseur = {'result': {'slots': [dict(creneau, actions=[
{'loadId': 'pac', 'kind': 'state', 'state': 3},
{'loadId': 'ecs', 'kind': 'setpoint', 'powerW': 1000.0}])]}}
a = o.actions_courantes(regles, maintenant)
self.assertEqual(o.differences(a, o.actions_courantes(optimiseur, maintenant), 0.5), [])
optimiseur['result']['slots'][0]['actions'][0]['state'] = 4
self.assertEqual(o.differences(a, o.actions_courantes(optimiseur, maintenant), 0.5), ['pac : état 3 → 4'])
optimiseur['result']['slots'][0]['actions'][0]['state'] = 3
optimiseur['result']['slots'][0]['actions'][1]['powerW'] = 1200
self.assertEqual(o.differences(a, o.actions_courantes(optimiseur, maintenant), 0.5),
['ecs : 1000 W → 1200 W'])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main(verbosity=2) unittest.main(verbosity=2)

View File

@ -13,7 +13,10 @@ de la trace (jamais d'écrasement), puis un résumé : requêtes, taux d'abstent
médian et max, cycles dont les actions diffèrent du plan enregistré. médian et max, cycles dont les actions diffèrent du plan enregistré.
« Actions du plan » : celles du créneau qui couvre l'instant de la requête (`timestamp`) — « Actions du plan » : celles du créneau qui couvre l'instant de la requête (`timestamp`) —
charge, kind, puissance (à --tolerance-w près), état SG-Ready. Une abstention (`abstain`, aucun charge, kind, puissance d'une consigne (`setpoint`, à --tolerance-w près), état d'une action
`state` (SG-Ready). La puissance d'une action `state` n'est pas comparée : le protocole ne la
définit pas (§6), et le plan du rule-based la porte (puissance estimée) quand un optimiseur ne la
donne pas. Une abstention (`abstain`, aucun
créneau, aucun créneau courant) n'a pas d'actions. Une ligne dont la réponse enregistrée est une créneau, aucun créneau courant) n'a pas d'actions. Une ligne dont la réponse enregistrée est une
erreur (`no-response`, `timeout`…) n'est pas comparable : comptée à part. erreur (`no-response`, `timeout`…) n'est pas comparable : comptée à part.
@ -156,8 +159,9 @@ def actions_courantes(reponse, maintenant):
continue continue
actions = [] actions = []
for a in creneau.get('actions') or []: for a in creneau.get('actions') or []:
actions.append((str(a.get('loadId')), str(a.get('kind', 'setpoint')), kind = str(a.get('kind', 'setpoint'))
a.get('powerW'), a.get('state'))) actions.append((str(a.get('loadId')), kind,
None if kind == 'state' else a.get('powerW'), a.get('state')))
return sorted(actions, key=lambda x: (x[0], x[1])) return sorted(actions, key=lambda x: (x[0], x[1]))
return [] return []