diff --git a/README.md b/README.md index 4e736d2..e45dcaf 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ This repository implements a production-ready skeleton for OpenEnergySphere's federated, contract-driven cross-domain energy planning platform. The MVP focuses on the EnergiBridge interoperability layer, mapping OpenEnergySphere primitives to a canonical IR and enabling plug-and-play adapters across DERs, meters, pumps, and buildings while preserving offline-first operation and governance. +Note: Added a minimal EnergiBridge mapper (EnergiBridgeMapper) to illustrate canonical mappings between LocalProblem objects and a vendor-agnostic IR. This enables straightforward extension to more complex cross-domain workflows and adapters. + Architecture highlights - Core primitives: LocalProblem, SharedVariables, DualVariables, PlanDelta, PrivacyBudget, AuditLog - Graph-of-Contracts registry for adapters and data schemas with per-message metadata diff --git a/src/energysphere/__init__.py b/src/energysphere/__init__.py index 0c4a178..c6fa6ba 100644 --- a/src/energysphere/__init__.py +++ b/src/energysphere/__init__.py @@ -1,2 +1,9 @@ """ Energysphere core package minimal init. """ __version__ = "0.1.0" + +# Re-export common bridge utilities for quick access +try: + from .energi_bridge import EnergiBridgeMapper # type: ignore +except Exception: + # If optional or during early import stages, fail gracefully. + EnergiBridgeMapper = None # type: ignore diff --git a/src/energysphere/energi_bridge.py b/src/energysphere/energi_bridge.py new file mode 100644 index 0000000..d20490f --- /dev/null +++ b/src/energysphere/energi_bridge.py @@ -0,0 +1,55 @@ +"""EnergiBridge: minimal canonical mapping for cross-domain interoperability. + +This is a small MVP shim that translates Energysphere primitives into a +vendor-agnostic IR representation (very light-weight). The goal is to provide a +stable, testable layer that can be expanded to full CatOpt-like mappings while +preserving offline-first semantics and governance hooks in later iterations. +""" + +from __future__ import annotations + +from typing import Any, Dict + +from .models import LocalProblem + + +class EnergiBridgeMapper: + """Lightweight translator between Energysphere primitives and a canonical IR. + + Only a tiny subset is implemented to keep MVP surface small: + - LocalProblem becomes a canonical Object with a few core fields. + - SharedVariables/PlanDelta could be extended similarly in future iterations. + """ + + def __init__(self, namespace: str = "default") -> None: + self.namespace = namespace + + def map_local_problem(self, lp: LocalProblem) -> Dict[str, Any]: + """Map a LocalProblem to a canonical Object representation. + + The structure is intentionally simple but stable for downstream adapters + to consume. It can be extended with versioning, provenance, and crypto + tags in future iterations. + """ + return { + "type": "Object", + "namespace": self.namespace, + "site_id": lp.site_id, + "objective": lp.objective, + "parameters": lp.parameters, + "version": 1, + } + + def map_object_to_local_problem(self, obj: Dict[str, Any]) -> LocalProblem: + """Inverse mapping from canonical Object to LocalProblem when possible.""" + if obj.get("type") != "Object": + raise ValueError("Unsupported canonical type: expected Object") + # Basic reconstruction; in MVP we ignore extra fields safely. + return LocalProblem( + site_id=obj.get("site_id", ""), + objective=obj.get("objective", ""), + parameters=obj.get("parameters", {}), + ) + + +__all__ = ["EnergiBridgeMapper"] diff --git a/tests/test_energi_bridge.py b/tests/test_energi_bridge.py new file mode 100644 index 0000000..7d43622 --- /dev/null +++ b/tests/test_energi_bridge.py @@ -0,0 +1,22 @@ +from energysphere.models import LocalProblem +from energysphere.energi_bridge import EnergiBridgeMapper + + +def test_map_local_problem_to_object(): + lp = LocalProblem(site_id="site-42", objective="min-cost", parameters={"load": 2.5}) + mapper = EnergiBridgeMapper(namespace="test-ns") + obj = mapper.map_local_problem(lp) + assert obj["type"] == "Object" + assert obj["site_id"] == "site-42" + assert obj["namespace"] == "test-ns" + assert obj["version"] == 1 + + +def test_map_object_to_local_problem_roundtrip(): + lp = LocalProblem(site_id="site-7", objective="min-cost", parameters={"p": 1}) + mapper = EnergiBridgeMapper() + obj = mapper.map_local_problem(lp) + lp2 = mapper.map_object_to_local_problem(obj) + assert lp2.site_id == lp.site_id + assert lp2.objective == lp.objective + assert lp2.parameters == lp.parameters