diff --git a/gridresilience_studio/__init__.py b/gridresilience_studio/__init__.py index 6b1c03e..de9f8b0 100644 --- a/gridresilience_studio/__init__.py +++ b/gridresilience_studio/__init__.py @@ -1,42 +1,34 @@ -"""GridResilience Studio core package (minimal bootstrap). +"""GridResilience Studio public package.""" -This repository adds a production-oriented, offline-first cross-domain orchestrator -foundation. This bootstrap provides a starting point for the EnergiBridge canonical -interoperability layer and related primitives used across adapters. +from .bridge import EnergiBridge +from .core import ( + AuditLog, + DeltaStore, + DualVariables, + Morphism, + Object, + PlanDelta, + PrivacyBudget, + RegistryEntry, + build_sample_world, +) +from .governance import GovernanceEntry, GovernanceLedger +from .offline_sync import DeltaSyncEngine +from .registry import GraphRegistry -Note: This module also patches on import to ensure a consistent API surface across -environments where the installed distribution might differ from the in-repo sources. -""" - -# Lightweight runtime patch to ensure EnergiBridge API surface is consistent even if -# an installed distribution lacks the wrappers we rely on during tests. -try: - from . import bridge as _bridge # type: ignore - EnergiBridge = getattr(_bridge, "EnergiBridge", None) - if EnergiBridge is not None: - # Add missing direct API wrappers if they are not present - if not hasattr(EnergiBridge, "map_local_problem"): - def _map_local_problem(self, local_problem): - return {"type": "Objects", "local_problem": local_problem} - EnergiBridge.map_local_problem = _map_local_problem # type: ignore - if not hasattr(EnergiBridge, "map_shared_signals"): - def _map_shared_signals(self, shared_signals): - return {"type": "Morphisms", "shared_signals": shared_signals} - EnergiBridge.map_shared_signals = _map_shared_signals # type: ignore - if not hasattr(EnergiBridge, "map_plan_delta"): - def _map_plan_delta(self, plan_delta): - return {"type": "PlanDelta", "delta": plan_delta} - EnergiBridge.map_plan_delta = _map_plan_delta # type: ignore - if not hasattr(EnergiBridge, "register_adapter"): - def _register_adapter(self, adapter_id, contract_version, data_contract): - entry = { - "adapter_id": adapter_id, - "contract_version": contract_version, - "data_contract": data_contract, - } - self._registry[adapter_id] = entry - return entry - EnergiBridge.register_adapter = _register_adapter # type: ignore -except Exception: - # Be permissive in patching; if anything goes wrong, tests should still run - pass +__all__ = [ + "Object", + "Morphism", + "PlanDelta", + "DualVariables", + "AuditLog", + "PrivacyBudget", + "RegistryEntry", + "DeltaStore", + "build_sample_world", + "DeltaSyncEngine", + "GraphRegistry", + "GovernanceEntry", + "GovernanceLedger", + "EnergiBridge", +] diff --git a/gridresilience_studio/core.py b/gridresilience_studio/core.py new file mode 100644 index 0000000..0eb91be --- /dev/null +++ b/gridresilience_studio/core.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List + + +@dataclass +class Object: + """Canonical object describing a device or asset.""" + + id: str + type: str + properties: Dict[str, Any] = field(default_factory=dict) + + def __post_init__(self) -> None: + if not self.id: + raise ValueError("Object requires an id") + + +@dataclass +class Morphism: + """Canonical morphism describing a versioned signal edge.""" + + id: str + source: str + target: str + signals: Dict[str, Any] = field(default_factory=dict) + version: int = 0 + + def bump(self) -> None: + self.version += 1 + + +@dataclass +class PlanDelta: + """Incremental islanding or load-shedding update.""" + + delta_id: str + islanded: bool = False + actions: List[Dict[str, Any]] = field(default_factory=list) + tags: Dict[str, str] = field(default_factory=dict) + + def add_action(self, action: Dict[str, Any]) -> None: + self.actions.append(action) + + +@dataclass +class DualVariables: + id: str + price_vector: List[float] = field(default_factory=list) + feasibility: float = 0.0 + version: int = 0 + + +@dataclass +class AuditLog: + entry: str + signer: str + timestamp: str + contract_id: str + version: int = 0 + + +@dataclass +class PrivacyBudget: + signal: str + budget: float + remaining: float + expiry: str + + +@dataclass +class RegistryEntry: + adapter_id: str + contract_version: str + data_contract: Dict[str, Any] = field(default_factory=dict) + + +class DeltaStore: + def __init__(self) -> None: + self.objects: Dict[str, Object] = {} + self.morphisms: Dict[str, Morphism] = {} + self.deltas: List[PlanDelta] = [] + + def add_object(self, obj: Object) -> None: + self.objects[obj.id] = obj + + def add_morphism(self, morph: Morphism) -> None: + self.morphisms[morph.id] = morph + + def add_delta(self, delta: PlanDelta) -> None: + self.deltas.append(delta) + + +def build_sample_world() -> DeltaStore: + ds = DeltaStore() + ds.add_object(Object(id="DER1", type="DER", properties={"rated_kW": 500, "location": "SiteA"})) + ds.add_object(Object(id="LOAD1", type="Load", properties={"critical": True, "rating_kW": 150})) + ds.add_morphism(Morphism(id="SIG1", source="DER1", target="LOAD1", signals={"voltage": 1.0}, version=1)) + ds.add_delta(PlanDelta(delta_id="DELTA1", islanded=True, actions=[{"type": "island", "target": "LOAD1"}], tags={"sig": "v1"})) + return ds diff --git a/gridresilience_studio/governance.py b/gridresilience_studio/governance.py new file mode 100644 index 0000000..a3bb2c6 --- /dev/null +++ b/gridresilience_studio/governance.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass +from datetime import datetime +from typing import List + + +@dataclass +class GovernanceEntry: + contract_id: str + action: str + signer: str + timestamp: str + version: int + payload_hash: str | None = None + + +class GovernanceLedger: + def __init__(self) -> None: + self._entries: List[GovernanceEntry] = [] + self._last_hash: str | None = None + + def _hash_entry(self, entry: GovernanceEntry) -> str: + data = { + "contract_id": entry.contract_id, + "action": entry.action, + "signer": entry.signer, + "timestamp": entry.timestamp, + "version": entry.version, + "payload_hash": entry.payload_hash, + "prev_hash": self._last_hash, + } + return hashlib.sha256(json.dumps(data, sort_keys=True).encode("utf-8")).hexdigest() + + def append(self, contract_id: str, action: str, signer: str, version: int, payload_hash: str | None = None) -> GovernanceEntry: + entry = GovernanceEntry( + contract_id=contract_id, + action=action, + signer=signer, + timestamp=datetime.utcnow().isoformat() + "Z", + version=version, + payload_hash=payload_hash, + ) + self._entries.append(entry) + self._last_hash = self._hash_entry(entry) + return entry + + def entries(self) -> List[GovernanceEntry]: + return list(self._entries) + + def latest_hash(self) -> str | None: + return self._last_hash + + +__all__ = ["GovernanceLedger", "GovernanceEntry"] diff --git a/gridresilience_studio/offline_sync.py b/gridresilience_studio/offline_sync.py new file mode 100644 index 0000000..3573247 --- /dev/null +++ b/gridresilience_studio/offline_sync.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import List, Tuple + +from .core import DeltaStore, Morphism, Object, PlanDelta + + +class DeltaSyncEngine: + """Lightweight offline-first delta-sync engine.""" + + def __init__(self, store: DeltaStore | None = None) -> None: + self.store = store or DeltaStore() + self.remote_version = 0 + self.local_version = 0 + + def apply_delta(self, delta: PlanDelta) -> None: + if any(existing.delta_id == delta.delta_id for existing in self.store.deltas): + return + self.store.add_delta(delta) + self.local_version += 1 + + def snapshot(self) -> List[PlanDelta]: + return list(self.store.deltas) + + def delta_with_cipher(self, delta: PlanDelta) -> Tuple[PlanDelta, str]: + delta.tags["hash"] = f"hash-{delta.delta_id}-{self.local_version}" + return delta, delta.tags["hash"] + + +__all__ = ["DeltaSyncEngine", "DeltaStore", "Object", "Morphism", "PlanDelta"] diff --git a/gridresilience_studio/registry.py b/gridresilience_studio/registry.py new file mode 100644 index 0000000..86ef0bd --- /dev/null +++ b/gridresilience_studio/registry.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, Optional + + +@dataclass +class RegistryEntry: + adapter_id: str + contract_version: str + data_contract: Dict[str, Any] = field(default_factory=dict) + metadata: Dict[str, Any] = field(default_factory=dict) + + +class GraphRegistry: + def __init__(self) -> None: + self._entries: Dict[str, RegistryEntry] = {} + + def add_entry(self, entry: RegistryEntry) -> None: + self._entries[entry.adapter_id] = entry + + def get_entry(self, adapter_id: str) -> Optional[RegistryEntry]: + return self._entries.get(adapter_id) + + def list_entries(self) -> Dict[str, RegistryEntry]: + return dict(self._entries) + + +__all__ = ["GraphRegistry", "RegistryEntry"]