From f42dc709e3b70420838a946eed3906baecdd3be3 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 17 Oct 2026 13:16:43 +0200 Subject: [PATCH] =?UTF-8?q?fix(tools):=20trace-replay=20compare=20une=20ac?= =?UTF-8?q?tion=20state=20sur=20son=20=C3=A9tat=20seul=20(brief=204=20T7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../tools/etm-powersync-trace-replay-test.py | 30 +++++++++++++++++++ tools/etm-powersync-trace-replay | 10 +++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/tests/tools/etm-powersync-trace-replay-test.py b/tests/tools/etm-powersync-trace-replay-test.py index a1faa1b..044df0a 100644 --- a/tests/tools/etm-powersync-trace-replay-test.py +++ b/tests/tools/etm-powersync-trace-replay-test.py @@ -228,5 +228,35 @@ class TestTraceReplay(unittest.TestCase): 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__': unittest.main(verbosity=2) diff --git a/tools/etm-powersync-trace-replay b/tools/etm-powersync-trace-replay index ea045d8..d3cb6c1 100755 --- a/tools/etm-powersync-trace-replay +++ b/tools/etm-powersync-trace-replay @@ -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é. « 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 erreur (`no-response`, `timeout`…) n'est pas comparable : comptée à part. @@ -156,8 +159,9 @@ def actions_courantes(reponse, maintenant): continue actions = [] for a in creneau.get('actions') or []: - actions.append((str(a.get('loadId')), str(a.get('kind', 'setpoint')), - a.get('powerW'), a.get('state'))) + kind = str(a.get('kind', 'setpoint')) + 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 []