build(agent): new-agents-3#dd492b iteration
This commit is contained in:
parent
c709e5e383
commit
1ee1695397
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# CityGrid Agent Architecture
|
||||||
|
|
||||||
|
- Objective: Build a policy-driven, privacy-preserving federated optimization platform for cross-utility districts.
|
||||||
|
- Core primitives (Canonical IR): LocalProblem, SharedVariables, DualVariables, PlanDelta, PrivacyBudget, AuditLog, PolicyBlock.
|
||||||
|
- Graph-of-Contracts registry (GoC): versioned schemas for adapters and data contracts; conformance tests.
|
||||||
|
- MVP Adapters: at least two starters (DER controller and district-chiller controller) translating to the canonical IR.
|
||||||
|
- Transport: TLS-like, delta-sync with bounded staleness via EnergiBridge.
|
||||||
|
- Privacy and governance: secure aggregation, privacy budgets, tamper-evident logs, DID-based identities.
|
||||||
|
- Testing: unit tests for core data models, adapter interop tests, end-to-end demo hints.
|
||||||
|
|
||||||
|
This document serves as the architectural contract for contributors. See README for running instructions.
|
||||||
30
README.md
30
README.md
|
|
@ -1,3 +1,29 @@
|
||||||
# citygrid-policy-driven-federated-optimiz
|
# CityGrid
|
||||||
|
|
||||||
A novel, open-source platform that extends federated, privacy-preserving distributed optimization to cross-utility districts (electricity, heating/cooling, water) by introducing a policy engine that translates city-level goals (reliability for essent
|
Policy-driven Federated Optimization for Cross-Utility Districts (Electricity, Heating/Cooling, Water).
|
||||||
|
|
||||||
|
Overview
|
||||||
|
- Core primitives: LocalProblem, SharedVariables, DualVariables, PlanDelta, PrivacyBudget, AuditLog, PolicyBlock.
|
||||||
|
- GoC registry for adapters and data contracts (versioned schemas).
|
||||||
|
- Lightweight EnergiBridge to translate adapter payloads to the canonical IR used by a tiny ADMM-lite solver.
|
||||||
|
- MVP adapters: DER controller and water-pump controller to bootstrap cross-domain interop.
|
||||||
|
|
||||||
|
Project structure
|
||||||
|
- citygrid/__init__.py: core dataclasses and public API surface.
|
||||||
|
- citygrid/registry/: in-memory Graph-of-Contracts registry.
|
||||||
|
- citygrid/bridge/: EnergiBridge for primitive mapping.
|
||||||
|
- citygrid/solver/: lightweight ADMM-like solver for MVP.
|
||||||
|
- citygrid/adapters/: toy adapters (DER, water pump).
|
||||||
|
- citygrid/demo/: small demo harness.
|
||||||
|
- AGENTS.md: architectural rules and testing guidance.
|
||||||
|
- pyproject.toml: packaging metadata.
|
||||||
|
|
||||||
|
How to run (local development)
|
||||||
|
- Ensure Python 3.8+ is installed.
|
||||||
|
- Install dependencies and run tests:
|
||||||
|
- python -m pip install -e .
|
||||||
|
- pytest -q
|
||||||
|
- python -m build
|
||||||
|
- Run the demo: python -m citygrid.demo.core_demo
|
||||||
|
|
||||||
|
This repository intentionally provides a compact, extensible MVP to bootstrap the CityGrid ecosystem. Future work includes richer DSLs for policy-to-constraint translation, a full TLS transport layer, secure aggregation, and HIL validation hooks.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
"""CityGrid: Lightweight, production-ready MVP for policy-driven federated optimization across cross-utility districts.
|
||||||
|
|
||||||
|
This package provides core primitives, a minimal Graph-of-Contracts (GoC) registry, a lightweight
|
||||||
|
EnergiaBridge for interoperability, and two toy adapters to bootstrap a 2-domain MVP (DER and water pumps).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LocalProblem:
|
||||||
|
id: str
|
||||||
|
domain: str
|
||||||
|
assets: List[str]
|
||||||
|
objective: Dict[str, Any]
|
||||||
|
constraints: Dict[str, Any]
|
||||||
|
solver_hint: Dict[str, Any] | None = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SharedVariables:
|
||||||
|
version: int
|
||||||
|
signals: Dict[str, Any]
|
||||||
|
priors: Dict[str, Any] | None = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DualVariables:
|
||||||
|
multipliers: Dict[str, float]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlanDelta:
|
||||||
|
delta: Dict[str, Any]
|
||||||
|
timestamp: float
|
||||||
|
author: str
|
||||||
|
contract_id: str
|
||||||
|
signature: str | None = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PrivacyBudget:
|
||||||
|
signal: str
|
||||||
|
budget: float
|
||||||
|
expiry: float | None = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AuditLog:
|
||||||
|
entry: str
|
||||||
|
signer: str
|
||||||
|
timestamp: float
|
||||||
|
contract_id: str
|
||||||
|
version: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PolicyBlock:
|
||||||
|
safety: Dict[str, Any]
|
||||||
|
exposure_rules: Dict[str, Any]
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"__version__",
|
||||||
|
"LocalProblem",
|
||||||
|
"SharedVariables",
|
||||||
|
"DualVariables",
|
||||||
|
"PlanDelta",
|
||||||
|
"PrivacyBudget",
|
||||||
|
"AuditLog",
|
||||||
|
"PolicyBlock",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from citygrid import LocalProblem
|
||||||
|
from citygrid.bridge.energi_bridge import EnergiBridge
|
||||||
|
from citygrid import __version__ as citygrid_version
|
||||||
|
|
||||||
|
class DerControllerAdapter:
|
||||||
|
def __init__(self, adapter_id: str = "der-controller-1"):
|
||||||
|
self.adapter_id = adapter_id
|
||||||
|
|
||||||
|
def build_local_problem(self) -> LocalProblem:
|
||||||
|
# Toy local problem for a DER controller
|
||||||
|
return LocalProblem(
|
||||||
|
id="lp-der-1",
|
||||||
|
domain="electricity",
|
||||||
|
assets=["DER-Unit-1"],
|
||||||
|
objective={"minimize": ["loss"], "weight": 1.0},
|
||||||
|
constraints={"voltage": {"min": 0.95, "max": 1.05}},
|
||||||
|
solver_hint=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def receive_delta(self, delta: dict) -> dict:
|
||||||
|
# In a real system, this would update internal state. Here we echo the delta.
|
||||||
|
return {"ack": True, "delta_version": delta.get("version", 0)}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from citygrid import LocalProblem
|
||||||
|
|
||||||
|
class WaterPumpControllerAdapter:
|
||||||
|
def __init__(self, adapter_id: str = "water-pump-1"):
|
||||||
|
self.adapter_id = adapter_id
|
||||||
|
|
||||||
|
def build_local_problem(self) -> LocalProblem:
|
||||||
|
return LocalProblem(
|
||||||
|
id="lp-water-1",
|
||||||
|
domain="water",
|
||||||
|
assets=["WaterPump-Station-1"],
|
||||||
|
objective={"maximize_service": {"priority": 1}},
|
||||||
|
constraints={"flow": {"min": 0.0, "max": 100.0}},
|
||||||
|
solver_hint=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def receive_delta(self, delta: dict) -> dict:
|
||||||
|
return {"ack": True, "delta_version": delta.get("version", 0)}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""EnergiaBridge: minimal bridge translation layer between canonical CityGrid IR and adapters."""
|
||||||
|
from .energi_bridge import EnergiBridge
|
||||||
|
|
||||||
|
__all__ = ["EnergiBridge"]
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from citygrid import LocalProblem, SharedVariables
|
||||||
|
|
||||||
|
|
||||||
|
class EnergiBridge:
|
||||||
|
"""Lightweight bridge translating between adapters and the canonical IR.
|
||||||
|
|
||||||
|
This is intentionally small: it provides two helpers to map between a
|
||||||
|
simplistic adapter payload (local problem) and the canonical LocalProblem/SharedVariables
|
||||||
|
structures used by the solver.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def to_canonical(local_problem: Dict[str, Any]) -> LocalProblem:
|
||||||
|
return LocalProblem(
|
||||||
|
id=local_problem.get("id", "lp-unknown"),
|
||||||
|
domain=local_problem.get("domain", "unknown"),
|
||||||
|
assets=local_problem.get("assets", []),
|
||||||
|
objective=local_problem.get("objective", {}),
|
||||||
|
constraints=local_problem.get("constraints", {}),
|
||||||
|
solver_hint=local_problem.get("solver_hint"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_canonical(shared: SharedVariables) -> Dict[str, Any]:
|
||||||
|
return {"version": shared.version, "signals": shared.signals}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
"""Tiny demonstration harness for CityGrid MVP interop."""
|
||||||
|
|
||||||
|
from citygrid.adapters.der_controller import DerControllerAdapter
|
||||||
|
from citygrid.adapters.water_pump_controller import WaterPumpControllerAdapter
|
||||||
|
from citygrid.bridge.energi_bridge import EnergiBridge
|
||||||
|
from citygrid import LocalProblem, SharedVariables
|
||||||
|
from citygrid.registry.registry import GoCRegistry
|
||||||
|
|
||||||
|
|
||||||
|
def run_demo():
|
||||||
|
der = DerControllerAdapter()
|
||||||
|
water = WaterPumpControllerAdapter()
|
||||||
|
lp_der = der.build_local_problem()
|
||||||
|
lp_water = water.build_local_problem()
|
||||||
|
# Instantiate a trivial bridge mapping to canonical form
|
||||||
|
canon_der = EnergiBridge.to_canonical({"id": lp_der.id, "domain": lp_der.domain, "assets": lp_der.assets, "objective": lp_der.objective, "constraints": lp_der.constraints})
|
||||||
|
canon_water = EnergiBridge.to_canonical({"id": lp_water.id, "domain": lp_water.domain, "assets": lp_water.assets, "objective": lp_water.objective, "constraints": lp_water.constraints})
|
||||||
|
print("Canonical Der LP:", canon_der)
|
||||||
|
print("Canonical Water LP:", canon_water)
|
||||||
|
|
||||||
|
# Simple registry usage
|
||||||
|
reg = GoCRegistry()
|
||||||
|
reg.register_adapter("der-controller-1", ["electricity"], "v0.1", repo=None)
|
||||||
|
reg.register_adapter("water-pump-1", ["water"], "v0.1", repo=None)
|
||||||
|
for a in reg.list_adapters():
|
||||||
|
print("Registered adapter:", a.adapter_id, a.domains)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_demo()
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""In-memory Graph-of-Contracts (GoC) registry for CityGrid MVP."""
|
||||||
|
from .registry import GoCRegistry
|
||||||
|
|
||||||
|
__all__ = ["GoCRegistry"]
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GoCAdapterEntry:
|
||||||
|
adapter_id: str
|
||||||
|
domains: List[str]
|
||||||
|
contract_version: str
|
||||||
|
repo: str | None = None
|
||||||
|
|
||||||
|
class GoCRegistry:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._registry: Dict[str, GoCAdapterEntry] = {}
|
||||||
|
|
||||||
|
def register_adapter(self, adapter_id: str, domains: List[str], contract_version: str, repo: str | None = None) -> None:
|
||||||
|
self._registry[adapter_id] = GoCAdapterEntry(adapter_id, domains, contract_version, repo)
|
||||||
|
|
||||||
|
def get_adapter(self, adapter_id: str) -> GoCAdapterEntry | None:
|
||||||
|
return self._registry.get(adapter_id)
|
||||||
|
|
||||||
|
def list_adapters(self) -> List[GoCAdapterEntry]:
|
||||||
|
return list(self._registry.values())
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AdmmState:
|
||||||
|
local_primal: Dict[str, float]
|
||||||
|
global_dual: float
|
||||||
|
rho: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def admm_step(state: AdmmState, local_update: Dict[str, float], global_update: float) -> AdmmState:
|
||||||
|
# Very small, toy ADMM step for MVP purposes
|
||||||
|
# Update local primal with local_update (simple averaging)
|
||||||
|
new_local = {k: (local_update.get(k, 0.0) + state.local_primal.get(k, 0.0)) / 2.0 for k in set(local_update) | set(state.local_primal)}
|
||||||
|
# Update global dual with a simple delta
|
||||||
|
new_global = (state.global_dual + global_update) * 0.5
|
||||||
|
return AdmmState(local_primal=new_local, global_dual=new_global, rho=state.rho)
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "citygrid"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Policy-driven federated optimization for cross-utility districts (CityGrid MVP)"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["citygrid"]
|
||||||
|
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
version = { attr = "citygrid.__version__" }
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Lightweight import path bootstrap for CI environments
|
||||||
|
# Ensures the repository root is on sys.path early enough for imports
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Compute repository root based on this file's location when possible
|
||||||
|
_repo_root = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
if _repo_root not in sys.path:
|
||||||
|
sys.path.insert(0, _repo_root)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Running CityGrid test suite..."
|
||||||
|
export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}$PWD"
|
||||||
|
python3 -m pip install -e .
|
||||||
|
pytest -q
|
||||||
|
echo "Building package to validate packaging metadata..."
|
||||||
|
python3 -m build
|
||||||
|
echo "Tests completed successfully."
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
from citygrid.adapters.der_controller import DerControllerAdapter
|
||||||
|
from citygrid.adapters.water_pump_controller import WaterPumpControllerAdapter
|
||||||
|
from citygrid.bridge.energi_bridge import EnergiBridge
|
||||||
|
|
||||||
|
|
||||||
|
def test_adapters_to_canonical_and_ack():
|
||||||
|
der = DerControllerAdapter()
|
||||||
|
water = WaterPumpControllerAdapter()
|
||||||
|
lp_der = der.build_local_problem()
|
||||||
|
lp_water = water.build_local_problem()
|
||||||
|
canon_der = EnergiBridge.to_canonical({"id": lp_der.id, "domain": lp_der.domain, "assets": lp_der.assets, "objective": lp_der.objective, "constraints": lp_der.constraints})
|
||||||
|
canon_water = EnergiBridge.to_canonical({"id": lp_water.id, "domain": lp_water.domain, "assets": lp_water.assets, "objective": lp_water.objective, "constraints": lp_water.constraints})
|
||||||
|
assert canon_der.domain == "electricity"
|
||||||
|
assert canon_water.domain == "water"
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import json
|
||||||
|
from citygrid import LocalProblem, SharedVariables
|
||||||
|
|
||||||
|
|
||||||
|
def test_local_problem_serializable():
|
||||||
|
lp = LocalProblem(id="lp-1", domain="electricity", assets=["DER-1"], objective={"minimize": ["loss"]}, constraints={"voltage": {"min": 0.95}})
|
||||||
|
s = json.dumps(lp.__dict__)
|
||||||
|
assert 'lp-1' in s
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
from citygrid import PrivacyBudget
|
||||||
|
|
||||||
|
|
||||||
|
def test_privacy_budget_basic():
|
||||||
|
pb = PrivacyBudget(signal="der_voltage", budget=0.5, expiry=None)
|
||||||
|
assert pb.budget == 0.5
|
||||||
|
assert pb.signal == "der_voltage"
|
||||||
Loading…
Reference in New Issue