From 3eb87a9f9491632f95f03fa0e440e7e65e8f695f Mon Sep 17 00:00:00 2001 From: agent-dd492b85242a98c5 Date: Mon, 20 Apr 2026 14:16:15 +0200 Subject: [PATCH] build(agent): new-agents-3#dd492b iteration --- README.md | 6 ++ src/gridresilience_studio/energi_bridge.py | 70 +++++++++++++++++++++- tests/test_energi_bridge.py | 41 +++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 tests/test_energi_bridge.py diff --git a/README.md b/README.md index d536085..5b9c895 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ Offline-first cross-domain orchestration for disaster-resilient grids. - Simulation and hardware-in-the-loop testing with KPI dashboards. - Governance ledger and event sourcing for auditability. +### EnergiBridge Enhancements +- Introduced EnergiBridge extensions to map GridResilience primitives (LocalProblem/LocalDevicePlans, SharedSignals, PlanDelta) to a vendor-agnostic, cross-domain representation and back. +- Added deterministic delta reconciliation (reconcile_deltas) to merge islanding/load-shedding updates across partitions while preserving cryptographic tags and metadata. +- Added tests validating object/morphism mapping and delta reconciliation to ensure offline-first consistency. +- Kept the surface lightweight and dependency-free for rapid integration with existing adapters. + Project structure and packaging - Python-based core with adapters under src/gridresilience_studio/adapters. - Core primitives live in src/gridresilience_studio/core.py. diff --git a/src/gridresilience_studio/energi_bridge.py b/src/gridresilience_studio/energi_bridge.py index 0a802b3..fc94ae6 100644 --- a/src/gridresilience_studio/energi_bridge.py +++ b/src/gridresilience_studio/energi_bridge.py @@ -8,7 +8,7 @@ tests fast and focused on integration surface. """ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Dict +from typing import Any, Dict, List from .core import Object, Morphism, PlanDelta @@ -49,6 +49,74 @@ class EnergiBridge: """Identity mapping for PlanDelta (pass-through in this sketch).""" return delta + # --------------------------------------------------------------------- + # Cross-domain canonical representations (GoC-style) + # --------------------------------------------------------------------- + @staticmethod + def to_goc_object(obj: Object) -> Dict[str, Any]: + """Translate a GridResilience Object into a simple GoC payload.""" + return { + "type": "Object", + "id": obj.id, + "class": obj.type, + "properties": obj.properties, + } + + @staticmethod + def to_goc_morphism(morph: Morphism) -> Dict[str, Any]: + """Translate a GridResilience Morphism into a simple GoC payload.""" + return { + "type": "Morphism", + "id": morph.id, + "source": morph.source, + "target": morph.target, + "signals": morph.signals, + "version": morph.version, + } + + @staticmethod + def reconcile_deltas(local_deltas: List[PlanDelta], remote_deltas: List[PlanDelta]) -> List[PlanDelta]: + """Deterministically reconcile two sets of PlanDelta objects. + + - Delta IDs are used as the canonical key. + - Actions from both sides are merged (without duplicating identical actions). + - islanded flag is OR'ed; tags are merged shallowly. + - Returns a new list of PlanDelta instances representing the merged view. + """ + merged: dict[str, PlanDelta] = {} + + # seed with local deltas + for d in local_deltas: + merged[d.delta_id] = PlanDelta( + delta_id=d.delta_id, + islanded=d.islanded, + actions=list(d.actions), + tags=dict(d.tags), + ) + + # merge remote deltas + for d in remote_deltas: + if d.delta_id in merged: + base = merged[d.delta_id] + # merge actions without duplicates + existing = list(base.actions) + for act in d.actions: + if act not in existing: + existing.append(act) + base.actions = existing + base.islanded = base.islanded or d.islanded + # shallow merge of tags + base.tags = {**base.tags, **d.tags} + else: + merged[d.delta_id] = PlanDelta( + delta_id=d.delta_id, + islanded=d.islanded, + actions=list(d.actions), + tags=dict(d.tags), + ) + + return list(merged.values()) + __all__ = ["LocalProblem", "SharedSignals", "EnergiBridge"] diff --git a/tests/test_energi_bridge.py b/tests/test_energi_bridge.py new file mode 100644 index 0000000..9f5da9a --- /dev/null +++ b/tests/test_energi_bridge.py @@ -0,0 +1,41 @@ +import pytest + +from gridresilience_studio.energi_bridge import EnergiBridge, LocalProblem, SharedSignals +from gridresilience_studio.core import Object, Morphism, PlanDelta + + +def test_goC_object_mapping(): + lp = LocalProblem(id="LP1", description="Test LocalProblem", resources={"foo": "bar"}) + obj = EnergiBridge.map_object(lp) + assert isinstance(obj, Object) + goc = EnergiBridge.to_goc_object(obj) + assert goc["type"] == "Object" + assert goc["id"] == "LP1" + assert goc["class"] == "LocalProblem" or goc["class"] == obj.type + assert isinstance(goc["properties"], dict) + + +def test_goC_morphism_mapping(): + sig = SharedSignals(id="SIG1", source="LP1", target="LP2", signals={"voltage": 1.0}) + morph = EnergiBridge.map_signals(sig) + assert isinstance(morph, Morphism) + gocm = EnergiBridge.to_goc_morphism(morph) + assert gocm["type"] == "Morphism" + assert gocm["id"] == "SIG1" + assert gocm["source"] == "LP1" + assert gocm["signals"] == {"voltage": 1.0} + + +def test_delta_reconciliation(): + d_local = PlanDelta(delta_id="D1", islanded=False, actions=[{"type": "noop"}], tags={}) + d_remote = PlanDelta(delta_id="D1", islanded=True, actions=[{"type": "toggle"}], tags={"t": "v1"}) + merged = EnergiBridge.reconcile_deltas([d_local], [d_remote]) + assert len(merged) == 1 + merged_delta = merged[0] + assert merged_delta.delta_id == "D1" + # both actions should be merged without duplicates + actions = merged_delta.actions + assert any(a.get("type") == "noop" for a in actions) + assert any(a.get("type") == "toggle" for a in actions) + # islanded should be True due to OR + assert merged_delta.islanded is True