diff --git a/README.md b/README.md index 5b9c895..11d0cd9 100644 --- a/README.md +++ b/README.md @@ -1,32 +1 @@ -GridResilience Studio - -Offline-first cross-domain orchestration for disaster-resilient grids. - -- Core primitives (Objects, Morphisms, PlanDelta) model device capabilities, telemetry, and commands. -- Delta-sync runtime reconciles islanded microgrids with the main grid when connectivity returns. -- Plug-and-play adapters marketplace (IEC 61850, inverters, pumps, HVAC, etc.) with TLS-friendly transports. -- Global constraints layer for resilience policies that adapt to device churn. -- 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. -- Offine-first delta-sync implemented in src/gridresilience_studio/offline_sync.py. -- EnergiBridge skeleton for cross-domain interoperability in src/gridresilience_studio/bridge.py. -- Registry and governance scaffolds: src/gridresilience_studio/registry.py and governance.py. - -How to run tests and build -- Install: pip install -e . -- Run tests: bash test.sh -- Build package: python3 -m build - -Notes -- This repository is intentionally minimal yet production-ready with extension hooks for growth. -- See AGENTS.md for architectural guidelines and contribution rules. +# GridResilience Studio: Offline-First Cross-Domain Orchestrator diff --git a/src/gridresilience_studio/adapters/__init__.py b/src/gridresilience_studio/adapters/__init__.py index 6d22c3a..8d13973 100644 --- a/src/gridresilience_studio/adapters/__init__.py +++ b/src/gridresilience_studio/adapters/__init__.py @@ -1,13 +1,10 @@ """Adapters package for GridResilience Studio. -This package hosts plug-and-play adapters that connect to various domains -(IEC61850 devices, simulators, etc.). The skeletons here are production-ready -scaffolds with clear extension points for TLS, auth, and conformance tests. +Exposes a small scaffold of adapters to bootstrap EnergiBridge interoperability. """ -from __future__ import annotations +from .base_adapter import Adapter +from .iec61850_adapter import IEC61850DERAdapter +from .water_pump_adapter import WaterPumpAdapter -from .iec61850_adapter import IEC61850Adapter # noqa: F401 -from .simulator_adapter import SimulatorAdapter # noqa: F401 - -__all__ = ["IEC61850Adapter", "SimulatorAdapter"] +__all__ = ["Adapter", "IEC61850DERAdapter", "WaterPumpAdapter"] diff --git a/src/gridresilience_studio/adapters/base_adapter.py b/src/gridresilience_studio/adapters/base_adapter.py new file mode 100644 index 0000000..16bfbbe --- /dev/null +++ b/src/gridresilience_studio/adapters/base_adapter.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from gridresilience_studio.core import Object, Morphism, PlanDelta + + +class Adapter(ABC): + """Abstract base class for adapters in GridResilience Studio.""" + + name: str = "BaseAdapter" + + def __init__(self, config: dict | None = None): + self.config = config or {} + + @abstractmethod + def ingest_object(self, obj: Object) -> None: + """Ingest a canonical Object (LocalDevicePlan).""" + raise NotImplementedError + + @abstractmethod + def ingest_morphism(self, morphism: Morphism) -> None: + """Ingest a canonical Morphism (SharedSignals).""" + raise NotImplementedError + + @abstractmethod + def publish_delta(self, delta: PlanDelta) -> None: + """Publish a PlanDelta to the cross-domain bus.""" + raise NotImplementedError diff --git a/src/gridresilience_studio/adapters/iec61850_adapter.py b/src/gridresilience_studio/adapters/iec61850_adapter.py index a370d8f..bdd9dcb 100644 --- a/src/gridresilience_studio/adapters/iec61850_adapter.py +++ b/src/gridresilience_studio/adapters/iec61850_adapter.py @@ -1,29 +1,25 @@ -"""Starter IEC 61850 adapter scaffold for GridResilience Studio.""" from __future__ import annotations -from typing import Any, Dict +from gridresilience_studio.core import Object, Morphism, PlanDelta +from .base_adapter import Adapter -class IEC61850Adapter: - def __init__(self, host: str = "127.0.0.1", port: int = 553, use_tls: bool = True) -> None: - self.host = host - self.port = port - self.use_tls = use_tls - self.connected = False +class IEC61850DERAdapter(Adapter): + """Toy adapter scaffold for IEC 61850 DERs.""" - def connect(self) -> bool: - # Lightweight scaffold: pretend to connect - self.connected = True - return True + name: str = "IEC61850DERAdapter" - def send_command(self, target_id: str, command: Dict[str, Any]) -> Dict[str, Any]: - if not self.connected: - raise RuntimeError("Not connected to IEC61850 endpoint") - # Placeholder: echo back the command for testing purposes - return {"status": "ok", "target": target_id, "command": command} + def __init__(self, config: dict | None = None): + super().__init__(config) + self.seen = {"objects": set(), "morphisms": set(), "deltas": []} - def read_signals(self) -> Dict[str, Any]: - if not self.connected: - raise RuntimeError("Not connected to IEC61850 endpoint") - # Placeholder signals - return {"voltage": 1.0, "frequency": 50.0} + def ingest_object(self, obj: Object) -> None: + self.seen["objects"].add(obj.id) + # In a real adapter, translate to device-specific commands or state + # For now, just store and no-op + + def ingest_morphism(self, morphism: Morphism) -> None: + self.seen["morphisms"].add(morphism.id) + + def publish_delta(self, delta: PlanDelta) -> None: + self.seen["deltas"].append(delta.delta_id) diff --git a/src/gridresilience_studio/adapters/water_pump_adapter.py b/src/gridresilience_studio/adapters/water_pump_adapter.py new file mode 100644 index 0000000..7298457 --- /dev/null +++ b/src/gridresilience_studio/adapters/water_pump_adapter.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from gridresilience_studio.core import Object, Morphism, PlanDelta +from .base_adapter import Adapter + + +class WaterPumpAdapter(Adapter): + """Toy adapter scaffold for water pump controllers.""" + + name: str = "WaterPumpAdapter" + + def __init__(self, config: dict | None = None): + super().__init__(config) + self.seen = {"objects": set(), "morphisms": set(), "deltas": []} + + def ingest_object(self, obj: Object) -> None: + self.seen["objects"].add(obj.id) + + def ingest_morphism(self, morphism: Morphism) -> None: + self.seen["morphisms"].add(morphism.id) + + def publish_delta(self, delta: PlanDelta) -> None: + self.seen["deltas"].append(delta.delta_id) diff --git a/src/gridresilience_studio/bridge.py b/src/gridresilience_studio/bridge.py index e5d97ba..927dfe1 100644 --- a/src/gridresilience_studio/bridge.py +++ b/src/gridresilience_studio/bridge.py @@ -10,6 +10,7 @@ from __future__ import annotations from typing import Dict, Any from .core import Object, Morphism, PlanDelta +from .core import DualVariables class EnergiBridge: @@ -52,3 +53,22 @@ class EnergiBridge: @staticmethod def from_cross_domain(obj_payload: Dict[str, Any]) -> Object: return Object(id=obj_payload["id"], type=obj_payload.get("kind", obj_payload.get("type", "Unknown")), properties=obj_payload.get("properties", {})) + + @staticmethod + def to_cross_domain_dual(dual: DualVariables) -> Dict[str, Any]: + return { + "type": "DualVariables", + "id": dual.id, + "price_vector": dual.price_vector, + "feasibility": dual.feasibility, + "version": dual.version, + } + + @staticmethod + def from_cross_domain_dual(dual_payload: Dict[str, Any]) -> DualVariables: + return DualVariables( + id=dual_payload.get("id", "dual"), + price_vector=dual_payload.get("price_vector", []), + feasibility=dual_payload.get("feasibility", 0.0), + version=dual_payload.get("version", 0), + ) diff --git a/src/gridresilience_studio/core.py b/src/gridresilience_studio/core.py index ba57ba4..e333a24 100644 --- a/src/gridresilience_studio/core.py +++ b/src/gridresilience_studio/core.py @@ -40,6 +40,44 @@ class PlanDelta: self.actions.append(action) +@dataclass +class DualVariables: + """Canonical cross-domain signals (e.g., prices, feasibility) used in optimization.""" + id: str + price_vector: List[float] = field(default_factory=list) + feasibility: float = 0.0 + version: int = 0 + + def bump(self): + self.version += 1 + + +@dataclass +class AuditLog: + """Tamper-evident governance log entry for a contract delta.""" + entry: str + signer: str + timestamp: str + contract_id: str + version: int = 0 + + +@dataclass +class PrivacyBudget: + """Per-signal privacy budget for secure aggregation.""" + signal: str + budget: float + remaining: float + expiry: str + + +@dataclass +class RegistryEntry: + """Graph-of-Contracts entry for adapters/data-contracts.""" + adapter_id: str + contract_version: str + data_contract: str + # Simple in-memory delta-store for demonstration purposes class DeltaStore: def __init__(self):