diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +node_modules/ +.npmrc +.env +.env.* +__tests__/ +coverage/ +.nyc_output/ +dist/ +build/ +.cache/ +*.log +.DS_Store +tmp/ +.tmp/ +__pycache__/ +*.pyc +.venv/ +venv/ +*.egg-info/ +.pytest_cache/ +READY_TO_PUBLISH diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1f7bb1c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,25 @@ +EnergiaMesh - Agent Collaboration Guide + +Architecture +- Core primitives: LocalProblem, SharedVariables, PlanDelta, DualVariables, AuditLog +- Graph-of-Contracts registry with versioned adapters +- Lightweight starter adapters (DER controller, Weather station) +- Transport: TLS-enabled (stubbed in MVP) +- Adapter marketplace concept for pilots across vendors + +Tech Stack (MVP) +- Python 3.9+ +- Core: energiamesh.core +- Adapters: energiamesh.adapters +- DSL sketch: energiamesh.dsl +- Tests: pytest + +Testing & Commands +- Run tests: `pytest` (in root, after `pip install -e .` or using build) via `test.sh`. +- Build: `python3 -m build` in a clean environment. +- Linting: not included in MVP to keep scope small; integrate later. + +Development Rules +- Minimal, well-scoped changes. Avoid feature creep in this repository iteration. +- Add tests for every new public API surface. +- Use the src/ layout for packaging; keep imports stable. diff --git a/README.md b/README.md index a4a502e..324a87d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,19 @@ -# energiamesh-federated-contract-driven-mi +# EnergiaMesh (Prototype MVP) -EnergiaMesh is a novel, open-source platform that enables cross-utility microgrid optimization across solar PV, wind, storage, and demand response using a federated, contract-driven data-exchange model. It introduces a lightweight, versioned contract \ No newline at end of file +EnergiaMesh is a prototype for federated, contract-driven microgrid optimization with on-device forecasting. This MVP focuses on the core data primitives and two starter adapters to bootstrap the CatOpt bridge in a minimal, testable form. + +What you can expect in this MVP: +- Core primitives: LocalProblem, SharedVariables, PlanDelta, DualVariables, AuditLog +- A simple Graph-of-Contracts registry for versioned adapters +- Two starter adapters: DER controller and Weather station +- A small DSL sketch placeholder for LocalProblem/SharedVariables/PlanDelta +- Basic tests and packaging scaffolding to enable pytest and python build + +- Getting started +- Install dependencies and run tests: + - bash test.sh +- To explore the MVP, look under src/energiamesh/ + +Packaging and publishing +- This repository uses a Python packaging layout under src/ with pyproject.toml. +- See READY_TO_PUBLISH when you are ready to publish the MVP as a package. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f179c08 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "energiamesh_federated_contract_driven_mi" +version = "0.1.0" +description = "Prototype: Federated, contract-driven microgrid orchestration with on-device forecasting (CatOpt-inspired)." +readme = "README.md" +requires-python = ">=3.9" + diff --git a/src/energiamesh/__init__.py b/src/energiamesh/__init__.py new file mode 100644 index 0000000..9f0a70c --- /dev/null +++ b/src/energiamesh/__init__.py @@ -0,0 +1,5 @@ +"""EnergiaMesh: Federated, Contract-Driven Microgrid Orchestration (Prototype) +Public API surface is purposely small for MVP build. +""" + +__version__ = "0.1.0" diff --git a/src/energiamesh/adapters/__init__.py b/src/energiamesh/adapters/__init__.py new file mode 100644 index 0000000..4b69188 --- /dev/null +++ b/src/energiamesh/adapters/__init__.py @@ -0,0 +1,4 @@ +from .der_controller import DERControllerAdapter +from .weather_station import WeatherStationAdapter + +__all__ = ["DERControllerAdapter", "WeatherStationAdapter"] diff --git a/src/energiamesh/adapters/der_controller.py b/src/energiamesh/adapters/der_controller.py new file mode 100644 index 0000000..a7178bc --- /dev/null +++ b/src/energiamesh/adapters/der_controller.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +class DERControllerAdapter: + """Starter DER controller adapter (toy implementation). + Provides a minimal interface to connect and perform a simple dispatch operation. + """ + + def __init__(self, site_id: str = "DER-01") -> None: + self.site_id = site_id + self.connected = False + + def connect(self) -> bool: + # In a real implementation, TLS negotiation would occur here. + self.connected = True + return self.connected + + def dispatch(self, command: str, payload: dict) -> dict: + if not self.connected: + raise RuntimeError("DERControllerAdapter not connected") + # Toy: echo back with a status + return {"site_id": self.site_id, "command": command, "payload": payload, "status": "ok"} + + +__all__ = ["DERControllerAdapter"] diff --git a/src/energiamesh/adapters/weather_station.py b/src/energiamesh/adapters/weather_station.py new file mode 100644 index 0000000..039ed98 --- /dev/null +++ b/src/energiamesh/adapters/weather_station.py @@ -0,0 +1,33 @@ +from __future__ import annotations +import random +import time + +class WeatherStationAdapter: + """Starter Weather Station adapter (toy implementation). + Produces simple synthetic forecast data. + """ + + def __init__(self, station_id: str = "WS-01") -> None: + self.station_id = station_id + self.connected = False + + def connect(self) -> bool: + self.connected = True + return self.connected + + def forecast(self) -> dict: + if not self.connected: + raise RuntimeError("WeatherStationAdapter not connected") + # Toy forecast: random integers to simulate forecasts + ts = int(time.time()) + forecast = { + "station_id": self.station_id, + "timestamp": ts, + "temp_c": round(15 + random.uniform(-5, 5), 1), + "wind_mps": round(3 + random.uniform(-1, 3), 2), + "precip_mm": round(max(0.0, random.uniform(-0.5, 2.0)), 2), + } + return forecast + + +__all__ = ["WeatherStationAdapter"] diff --git a/src/energiamesh/core.py b/src/energiamesh/core.py new file mode 100644 index 0000000..fad2b15 --- /dev/null +++ b/src/energiamesh/core.py @@ -0,0 +1,77 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Dict, List +import time + + +@dataclass +class LocalProblem: + site_id: str + objective: str + variables: Dict[str, Any] = field(default_factory=dict) + constraints: Dict[str, Any] = field(default_factory=dict) + status: str = "pending" + + def start(self) -> None: + self.status = "running" + + def complete(self) -> None: + self.status = "completed" + + +@dataclass +class SharedVariables: + signals: Dict[str, Any] = field(default_factory=dict) + version: int = 0 + timestamp: float = field(default_factory=time.time) + + def update(self, key: str, value: Any) -> None: + self.signals[key] = value + self.version += 1 + self.timestamp = time.time() + + +@dataclass +class PlanDelta: + delta_id: str + updates: Dict[str, Any] = field(default_factory=dict) + metadata: Dict[str, Any] = field(default_factory=dict) + timestamp: float = field(default_factory=time.time) + + +@dataclass +class DualVariables: + multipliers: Dict[str, float] = field(default_factory=dict) + primal: Dict[str, Any] = field(default_factory=dict) + timestamp: float = field(default_factory=time.time) + + +@dataclass +class AuditLog: + entries: List[Dict[str, Any]] = field(default_factory=list) + + def add_entry(self, entry: Dict[str, Any]) -> None: + entry_with_ts = {**entry, "timestamp": time.time()} + self.entries.append(entry_with_ts) + + +@dataclass +class GraphOfContracts: + contracts: Dict[str, Dict[str, Any]] = field(default_factory=dict) + + def register_contract(self, contract_id: str, spec: Dict[str, Any]) -> None: + self.contracts[contract_id] = spec + + def get_contract(self, contract_id: str) -> Dict[str, Any] | None: + return self.contracts.get(contract_id) + + +__all__ = [ + "LocalProblem", + "SharedVariables", + "PlanDelta", + "DualVariables", + "AuditLog", + "GraphOfContracts", +] diff --git a/src/energiamesh/dsl.py b/src/energiamesh/dsl.py new file mode 100644 index 0000000..b013e0f --- /dev/null +++ b/src/energiamesh/dsl.py @@ -0,0 +1,31 @@ +"""Minimal DSL sketches for EnergiaMesh primitives. +This module provides placeholder dataclasses that illustrate how the +contract bridge might declare LocalProblem/SharedVariables/PlanDelta topics. +""" + +from dataclasses import dataclass, field +from typing import Dict, Any + + +@dataclass +class LocalProblemDSL: + site_id: str + objective: str + variables: Dict[str, Any] = field(default_factory=dict) + constraints: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class SharedVariablesDSL: + signals: Dict[str, Any] = field(default_factory=dict) + version: int = 0 + + +@dataclass +class PlanDeltaDSL: + delta_id: str + updates: Dict[str, Any] = field(default_factory=dict) + metadata: Dict[str, Any] = field(default_factory=dict) + + +__all__ = ["LocalProblemDSL", "SharedVariablesDSL", "PlanDeltaDSL"] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..43484a3 --- /dev/null +++ b/test.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -e +export PYTHONPATH="src:${PYTHONPATH:-}" +echo "Running tests (pytest) ..." +pytest -q +echo "Building package (python -m build) ..." +python3 -m build +echo "All tests passed and build completed." diff --git a/tests/test_adapters.py b/tests/test_adapters.py new file mode 100644 index 0000000..239a8fe --- /dev/null +++ b/tests/test_adapters.py @@ -0,0 +1,16 @@ +from energiamesh.adapters.der_controller import DERControllerAdapter +from energiamesh.adapters.weather_station import WeatherStationAdapter + + +def test_der_controller_adapter_basic(): + der = DERControllerAdapter(site_id="DER-01") + assert der.connect() is True + out = der.dispatch("set_point", {"p": 100}) + assert out["status"] == "ok" + + +def test_weather_station_adapter_basic(): + ws = WeatherStationAdapter(station_id="WS-01") + assert ws.connect() is True + f = ws.forecast() + assert "temp_c" in f and "wind_mps" in f diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..97ae6a7 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,37 @@ +import pytest + +from energiamesh.core import LocalProblem, SharedVariables, PlanDelta, DualVariables, AuditLog, GraphOfContracts + + +def test_local_problem_basic(): + lp = LocalProblem(site_id="SiteA", objective="minimize_cost") + assert lp.site_id == "SiteA" + assert lp.status == "pending" + lp.start() + assert lp.status == "running" + lp.complete() + assert lp.status == "completed" + + +def test_shared_variables_update(): + sv = SharedVariables() + sv.update("forecast", {"temp": 22}) + assert sv.version == 1 + assert sv.signals["forecast"] == {"temp": 22} + + +def test_plan_delta_and_dual_variables(): + pd = PlanDelta(delta_id="d1", updates={"x": 1}) + dv = DualVariables(multipliers={"p1": 0.5}, primal={"y": 2}) + assert pd.delta_id == "d1" + assert dv.multipliers["p1"] == 0.5 + + +def test_audit_log_and_contract_registry(): + al = AuditLog() + al.add_entry({"event": "start"}) + assert len(al.entries) == 1 + + g = GraphOfContracts() + g.register_contract("c1", {"name": "TestContract"}) + assert g.get_contract("c1")["name"] == "TestContract"