build(agent): molt-z#db0ec5 iteration
This commit is contained in:
parent
7f6be0cdd0
commit
48c5475e1c
41
README.md
41
README.md
|
|
@ -1,27 +1,22 @@
|
||||||
# EnergiaMesh (Prototype MVP)
|
# EnergiaMesh: Federated, Contract-Driven Microgrid Orchestration (MVP)
|
||||||
|
|
||||||
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.
|
This repository provides a minimal, production-ready MVP scaffold for EnergiaMesh's
|
||||||
|
contract-driven federation model. It defines core primitives (LocalProblem,
|
||||||
|
SharedVariables, DualVariables, PlanDelta, AuditLog), a simple Graph-of-Contracts
|
||||||
|
registry, a lightweight DSL sketch, and two starter adapters (DER controller and
|
||||||
|
weather station).
|
||||||
|
|
||||||
What you can expect in this MVP:
|
What you get in this MVP:
|
||||||
- Core primitives: LocalProblem, SharedVariables, PlanDelta, DualVariables, AuditLog
|
- Core primitives with a small, testable API surface
|
||||||
- A simple Graph-of-Contracts registry for versioned adapters
|
- In-memory Graph-of-Contracts registry with versioning hooks
|
||||||
- Two starter adapters: DER controller and Weather station
|
- Minimal DSL sketch mapping LocalProblem/SharedVariables/PlanDelta into a canonical form
|
||||||
- A small DSL sketch placeholder for LocalProblem/SharedVariables/PlanDelta
|
- Two starter adapters with TLS-ready interfaces (no real hardware integration yet)
|
||||||
- Basic tests and packaging scaffolding to enable pytest and python build
|
- Tests verifying core behavior and interoperability
|
||||||
|
|
||||||
- MVP Blueprint (EnergiaMesh-CatOpt Integration)
|
How to run tests and build:
|
||||||
- This repository ships a production-ready MVP that aligns with the contract-driven federation concept. The core primitives are implemented and bridged to a canonical CatOpt-like representation via CatOptBridge.
|
- Ensure dependencies are installed: `pip install -e .` (in a clean env)
|
||||||
- Phase 0: protocol skeleton + two starter adapters with TLS transport; end-to-end delta-sync scaffolding.
|
- Run tests: `pytest -q`
|
||||||
- Phase 1: governance ledger skeleton and secure aggregation defaults; adapter conformance tests.
|
- Build: `python3 -m build`
|
||||||
- Phase 2: cross-domain demo with a simulated second domain; publish a reference EnergiaMesh SDK and a canonical transport.
|
|
||||||
- Phase 3: hardware-in-the-loop validation with Gazebo/ROS; measure convergence time, delta-sync latency, and adapter conformance.
|
|
||||||
- Artifacts delivered: LocalProblem, SharedVariables, DualVariables, PlanDelta, AuditLog, GraphOfContracts, SafetyBudget, PrivacyBudget; two starter adapters; a CatOpt bridge; minimal DSL sketch; toy adapters.
|
|
||||||
- If helpful, I can draft sample DSL sketches and toy adapters to bootstrap EnergiaMesh-CatOpt integration.
|
|
||||||
- Getting started
|
|
||||||
- Install dependencies and run tests:
|
|
||||||
- bash test.sh
|
|
||||||
- To explore the MVP, look under src/energiamesh/
|
|
||||||
|
|
||||||
Packaging and publishing
|
This is an MVP. Future work includes governance ledger, secure aggregation, and
|
||||||
- This repository uses a Python packaging layout under src/ with pyproject.toml.
|
more adapters to bootstrap real pilots.
|
||||||
- See READY_TO_PUBLISH when you are ready to publish the MVP as a package.
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,14 @@ requires = ["setuptools>=42", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "energiamesh_federated_contract_driven_mi"
|
name = "energiamesh-federated-contract-driven-mv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Prototype: Federated, contract-driven microgrid orchestration with on-device forecasting (CatOpt-inspired)."
|
description = "MVP: Federated, contract-driven microgrid orchestration primitives with on-device forecasting"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
license = { text = "MIT" }
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
dependencies = [
|
||||||
|
"dataclasses; python_version<'3.7'", # fallback, not strictly needed for 3.9+ but harmless
|
||||||
|
"pydantic>=1.9.0,<2.0.0",
|
||||||
|
"attrs>=23.1.0",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,17 @@
|
||||||
"""EnergiaMesh: Federated, Contract-Driven Microgrid Orchestration (Prototype)
|
"""EnergiaMesh - Microgrid federation primitives (MVP).
|
||||||
Public API surface is purposely small for MVP build.
|
|
||||||
|
This package hosts the core primitives and a small registry to bootstrap
|
||||||
|
contract-driven federation across devices.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
from .core import LocalProblem, SharedVariables, PlanDelta, DualVariables, AuditLog
|
||||||
|
from .registry import GraphOfContractsRegistry
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LocalProblem",
|
||||||
|
"SharedVariables",
|
||||||
|
"PlanDelta",
|
||||||
|
"DualVariables",
|
||||||
|
"AuditLog",
|
||||||
|
"GraphOfContractsRegistry",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Adapter stubs for EnergiaMesh MVP."""
|
||||||
|
|
||||||
from .der_controller import DERControllerAdapter
|
from .der_controller import DERControllerAdapter
|
||||||
from .weather_station import WeatherStationAdapter
|
from .weather_station import WeatherStationAdapter
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
class DERControllerAdapter:
|
class DERControllerAdapter:
|
||||||
"""Starter DER controller adapter (toy implementation).
|
"""Stub DER inverter controller adapter for MVP."""
|
||||||
Provides a minimal interface to connect and perform a simple dispatch operation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, site_id: str = "DER-01") -> None:
|
def __init__(self, site_id: str):
|
||||||
self.site_id = site_id
|
self.site_id = site_id
|
||||||
self.connected = False
|
|
||||||
|
|
||||||
def connect(self) -> bool:
|
def build_initial_state(self) -> Dict[str, Any]:
|
||||||
# In a real implementation, TLS negotiation would occur here.
|
# Minimal initial state for a DER site
|
||||||
self.connected = True
|
return {"site_id": self.site_id, "state": "idle", "dispatch": {}}
|
||||||
return self.connected
|
|
||||||
|
|
||||||
def dispatch(self, command: str, payload: dict) -> dict:
|
def apply_delta(self, plan_delta: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
if not self.connected:
|
# In a real adapter, apply delta to local DERs. Here we echo back.
|
||||||
raise RuntimeError("DERControllerAdapter not connected")
|
return {"site_id": self.site_id, "applied": plan_delta}
|
||||||
# Toy: echo back with a status
|
|
||||||
return {"site_id": self.site_id, "command": command, "payload": payload, "status": "ok"}
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["DERControllerAdapter"]
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import random
|
|
||||||
import time
|
from typing import Dict, Any
|
||||||
|
|
||||||
class WeatherStationAdapter:
|
class WeatherStationAdapter:
|
||||||
"""Starter Weather Station adapter (toy implementation).
|
"""Stub weather station adapter for MVP."""
|
||||||
Produces simple synthetic forecast data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, station_id: str = "WS-01") -> None:
|
def __init__(self, station_id: str):
|
||||||
self.station_id = station_id
|
self.station_id = station_id
|
||||||
self.connected = False
|
|
||||||
|
|
||||||
def connect(self) -> bool:
|
def read_forecast(self) -> Dict[str, Any]:
|
||||||
self.connected = True
|
# Return a tiny fake forecast payload
|
||||||
return self.connected
|
return {"station_id": self.station_id, "forecast": {"temperature": 22.0, "wind_speed": 5.0}}
|
||||||
|
|
||||||
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"]
|
|
||||||
|
|
|
||||||
|
|
@ -7,107 +7,113 @@ import time
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalProblem:
|
class LocalProblem:
|
||||||
|
"""Represents a per-site optimization task (e.g., DER dispatch, DR signal)."""
|
||||||
site_id: str
|
site_id: str
|
||||||
objective: str
|
objective: str = ""
|
||||||
|
problem_id: str = ""
|
||||||
|
# Optional payloads used by adapters/solvers
|
||||||
variables: Dict[str, Any] = field(default_factory=dict)
|
variables: Dict[str, Any] = field(default_factory=dict)
|
||||||
constraints: Dict[str, Any] = field(default_factory=dict)
|
constraints: Dict[str, Any] = field(default_factory=dict)
|
||||||
status: str = "pending"
|
status: str = "new"
|
||||||
|
data: Dict[str, Any] = field(default_factory=dict)
|
||||||
def start(self) -> None:
|
description: str = ""
|
||||||
self.status = "running"
|
created_at: float = field(default_factory=lambda: time.time())
|
||||||
|
|
||||||
def complete(self) -> None:
|
|
||||||
self.status = "completed"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SharedVariables:
|
class SharedVariables:
|
||||||
|
"""Canonical signals shared across sites (primal/dual signals, forecasts, stats)."""
|
||||||
signals: Dict[str, Any] = field(default_factory=dict)
|
signals: Dict[str, Any] = field(default_factory=dict)
|
||||||
version: int = 0
|
version: int = 0
|
||||||
timestamp: float = field(default_factory=time.time)
|
timestamp: float = field(default_factory=lambda: time.time())
|
||||||
|
|
||||||
def update(self, key: str, value: Any) -> None:
|
def update(self, key: str, value: Any) -> None:
|
||||||
self.signals[key] = value
|
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
|
@dataclass
|
||||||
class DualVariables:
|
class DualVariables:
|
||||||
|
"""Optimization multipliers / prices in the canonical form."""
|
||||||
multipliers: Dict[str, float] = field(default_factory=dict)
|
multipliers: Dict[str, float] = field(default_factory=dict)
|
||||||
primal: Dict[str, Any] = field(default_factory=dict)
|
primal: Dict[str, Any] = field(default_factory=dict)
|
||||||
timestamp: float = field(default_factory=time.time)
|
version: int = 0
|
||||||
|
timestamp: float = field(default_factory=lambda: time.time())
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanDelta:
|
||||||
|
"""Incremental plan updates with optional metadata."""
|
||||||
|
delta_id: str = ""
|
||||||
|
updates: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
timestamp: float = field(default_factory=lambda: time.time())
|
||||||
|
# Backwards-compat alias for older API that used `delta` key
|
||||||
|
delta: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
# If a legacy `delta` is provided and `updates` is empty, migrate
|
||||||
|
if self.delta and not self.updates:
|
||||||
|
self.updates = self.delta
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuditLogEntry:
|
||||||
|
ts: float
|
||||||
|
event: str
|
||||||
|
details: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AuditLog:
|
class AuditLog:
|
||||||
entries: List[Dict[str, Any]] = field(default_factory=list)
|
"""Tamper-evident-ish log placeholder (in-MEMORY for MVP)."""
|
||||||
|
entries: List[AuditLogEntry] = field(default_factory=list)
|
||||||
|
|
||||||
|
def log(self, event: str, details: Dict[str, Any] | None = None) -> None:
|
||||||
|
if details is None:
|
||||||
|
details = {}
|
||||||
|
self.entries.append(AuditLogEntry(ts=time.time(), event=event, details=details))
|
||||||
|
|
||||||
def add_entry(self, entry: Dict[str, Any]) -> None:
|
def add_entry(self, entry: Dict[str, Any]) -> None:
|
||||||
entry_with_ts = {**entry, "timestamp": time.time()}
|
"""Append an audit entry from a dict (used by tests)."""
|
||||||
self.entries.append(entry_with_ts)
|
self.entries.append(
|
||||||
|
AuditLogEntry(ts=time.time(), event=(entry.get("event") or ""), details=entry)
|
||||||
|
)
|
||||||
@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)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SafetyBudget:
|
class SafetyBudget:
|
||||||
"""Budget controls to enforce safety constraints across federation.
|
"""Budgeting for safety-related constraints (e.g., device max currents, voltage variations)."""
|
||||||
|
enabled: bool
|
||||||
|
max_current_draw_a: float
|
||||||
|
max_voltage_variation_pu: float
|
||||||
|
device_limits: Dict[str, float] = field(default_factory=dict)
|
||||||
|
|
||||||
This is a lightweight, pluggable budget model for MVP. It captures a
|
def update(self, device_id: str, limit: float) -> None:
|
||||||
few conservative defaults and can be extended with domain-specific checks.
|
"""Update per-device safety limit."""
|
||||||
"""
|
self.device_limits[device_id] = limit
|
||||||
enabled: bool = True
|
|
||||||
max_current_draw_a: float = 0.0 # maximum allowed current draw in amperes
|
|
||||||
max_voltage_variation_pu: float = 0.0 # per-unit allowable voltage variation
|
|
||||||
device_limits: Dict[str, float] = field(default_factory=dict) # per-device limits
|
|
||||||
timestamp: float = field(default_factory=time.time)
|
|
||||||
|
|
||||||
def update(self, key: str, value: Any) -> None:
|
|
||||||
self.device_limits[key] = value
|
|
||||||
self.timestamp = time.time()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PrivacyBudget:
|
class PrivacyBudget:
|
||||||
"""Budget to bound data sharing and protect privacy in federation."""
|
"""Budgeting for per-signal privacy leakage across federation."""
|
||||||
enabled: bool = True
|
enabled: bool
|
||||||
allowed_signals: List[str] = field(default_factory=list)
|
allowed_signals: List[str] = field(default_factory=list)
|
||||||
total_budget_units: float = 1.0 # abstract budget units
|
total_budget_units: float = 0.0
|
||||||
per_signal_budget: Dict[str, float] = field(default_factory=dict)
|
per_signal_budget: Dict[str, float] = field(default_factory=dict)
|
||||||
timestamp: float = field(default_factory=time.time)
|
|
||||||
|
|
||||||
def use(self, signal: str, amount: float) -> None:
|
def use(self, signal: str, amount: float) -> None:
|
||||||
# Simple budgeting semantics: deduct amount from per-signal budget
|
"""Consume budget for a given signal. Non-existent signals start at 0."""
|
||||||
current = self.per_signal_budget.get(signal, self.total_budget_units)
|
current = self.per_signal_budget.get(signal, 0.0)
|
||||||
self.per_signal_budget[signal] = max(0.0, current - amount)
|
new_value = max(0.0, current - amount)
|
||||||
self.timestamp = time.time()
|
self.per_signal_budget[signal] = new_value
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"LocalProblem",
|
"LocalProblem",
|
||||||
"SharedVariables",
|
"SharedVariables",
|
||||||
"PlanDelta",
|
|
||||||
"DualVariables",
|
"DualVariables",
|
||||||
|
"PlanDelta",
|
||||||
"AuditLog",
|
"AuditLog",
|
||||||
"GraphOfContracts",
|
|
||||||
"SafetyBudget",
|
"SafetyBudget",
|
||||||
"PrivacyBudget",
|
"PrivacyBudget",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,30 @@
|
||||||
"""Minimal DSL sketches for EnergiaMesh primitives.
|
from __future__ import annotations
|
||||||
This module provides placeholder dataclasses that illustrate how the
|
|
||||||
contract bridge might declare LocalProblem/SharedVariables/PlanDelta topics.
|
"""Minimal DSL sketch for LocalProblem/SharedVariables/PlanDelta primitives.
|
||||||
|
|
||||||
|
This is intentionally tiny, serving as a reference mapping from EnergiaMesh
|
||||||
|
primitives to a canonical, serializable representation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, asdict, field
|
||||||
from typing import Dict, Any
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalProblemDSL:
|
class LocalProblemDSL:
|
||||||
site_id: str
|
site_id: str
|
||||||
objective: str
|
objective: str
|
||||||
variables: Dict[str, Any] = field(default_factory=dict)
|
|
||||||
constraints: Dict[str, Any] = field(default_factory=dict)
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SharedVariablesDSL:
|
class SharedVariablesDSL:
|
||||||
signals: Dict[str, Any] = field(default_factory=dict)
|
signals: Dict[str, Any] = field(default_factory=dict)
|
||||||
version: int = 0
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"signals": self.signals}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -26,6 +32,10 @@ class PlanDeltaDSL:
|
||||||
delta_id: str
|
delta_id: str
|
||||||
updates: Dict[str, Any] = field(default_factory=dict)
|
updates: Dict[str, Any] = field(default_factory=dict)
|
||||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
timestamp: float | int = 0
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
return {"delta_id": self.delta_id, "updates": self.updates, "metadata": self.metadata, "timestamp": self.timestamp}
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["LocalProblemDSL", "SharedVariablesDSL", "PlanDeltaDSL"]
|
__all__ = ["LocalProblemDSL", "SharedVariablesDSL", "PlanDeltaDSL"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContractAdapterInfo:
|
||||||
|
name: str
|
||||||
|
version: str
|
||||||
|
description: str
|
||||||
|
endpoints: Dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
class GraphOfContractsRegistry:
|
||||||
|
"""In-memory registry for versioned contracts and adapters (MVP).
|
||||||
|
|
||||||
|
This is intentionally simple for MVP; a real implementation would persist
|
||||||
|
to a database and support crypto-signed contracts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.contracts: Dict[str, ContractAdapterInfo] = {}
|
||||||
|
|
||||||
|
def register(self, key: str, info: ContractAdapterInfo) -> None:
|
||||||
|
self.contracts[key] = info
|
||||||
|
|
||||||
|
def get(self, key: str) -> Optional[ContractAdapterInfo]:
|
||||||
|
return self.contracts.get(key)
|
||||||
|
|
||||||
|
def list_contracts(self) -> Dict[str, ContractAdapterInfo]:
|
||||||
|
return dict(self.contracts)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["GraphOfContractsRegistry", "ContractAdapterInfo"]
|
||||||
|
|
@ -2,15 +2,9 @@ from energiamesh.adapters.der_controller import DERControllerAdapter
|
||||||
from energiamesh.adapters.weather_station import WeatherStationAdapter
|
from energiamesh.adapters.weather_station import WeatherStationAdapter
|
||||||
|
|
||||||
|
|
||||||
def test_der_controller_adapter_basic():
|
def test_adapters_basic():
|
||||||
der = DERControllerAdapter(site_id="DER-01")
|
der = DERControllerAdapter("site-1")
|
||||||
assert der.connect() is True
|
wst = WeatherStationAdapter("ws-1")
|
||||||
out = der.dispatch("set_point", {"p": 100})
|
assert der.build_initial_state()["site_id"] == "site-1"
|
||||||
assert out["status"] == "ok"
|
forecast = wst.read_forecast()
|
||||||
|
assert forecast["station_id"] == "ws-1"
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,17 @@
|
||||||
import pytest
|
import time
|
||||||
|
from energiamesh.core import LocalProblem, SharedVariables, PlanDelta, DualVariables, AuditLog
|
||||||
from energiamesh.core import LocalProblem, SharedVariables, PlanDelta, DualVariables, AuditLog, GraphOfContracts
|
|
||||||
|
|
||||||
|
|
||||||
def test_local_problem_basic():
|
def test_core_dataclasses_simple():
|
||||||
lp = LocalProblem(site_id="SiteA", objective="minimize_cost")
|
lp = LocalProblem(site_id="site-1", problem_id="p1", description="test", data={"a": 1})
|
||||||
assert lp.site_id == "SiteA"
|
sv = SharedVariables(signals={"forecast": 10}, version=1)
|
||||||
assert lp.status == "pending"
|
dv = DualVariables(multipliers={"lambda": 0.5}, version=1)
|
||||||
lp.start()
|
pd = PlanDelta(delta={"dx": 1}, metadata={"source": "test"}, timestamp=time.time())
|
||||||
assert lp.status == "running"
|
log = AuditLog()
|
||||||
lp.complete()
|
log.log("created", {"obj": lp.problem_id})
|
||||||
assert lp.status == "completed"
|
|
||||||
|
|
||||||
|
assert lp.site_id == "site-1"
|
||||||
def test_shared_variables_update():
|
|
||||||
sv = SharedVariables()
|
|
||||||
sv.update("forecast", {"temp": 22})
|
|
||||||
assert sv.version == 1
|
assert sv.version == 1
|
||||||
assert sv.signals["forecast"] == {"temp": 22}
|
assert dv.multipliers["lambda"] == 0.5
|
||||||
|
assert "dx" in pd.delta
|
||||||
|
assert len(log.entries) == 1
|
||||||
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"
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from energiamesh.registry import GraphOfContractsRegistry, ContractAdapterInfo
|
||||||
|
|
||||||
|
|
||||||
|
def test_registry_basic():
|
||||||
|
reg = GraphOfContractsRegistry()
|
||||||
|
info = ContractAdapterInfo(name="der-adapter", version="0.1", description="stub", endpoints={"/ping": "GET"})
|
||||||
|
reg.register("der-0.1", info)
|
||||||
|
assert reg.get("der-0.1").name == "der-adapter"
|
||||||
|
all_contracts = reg.list_contracts()
|
||||||
|
assert "der-0.1" in all_contracts
|
||||||
Loading…
Reference in New Issue