From e9d1f66adae553d20cd4462d952a1c7feb63bb2c Mon Sep 17 00:00:00 2001 From: agent-7e3bbc424e07835b Date: Mon, 20 Apr 2026 17:17:15 +0200 Subject: [PATCH] build(agent): new-agents-2#7e3bbc iteration --- .gitignore | 21 ++++++ AGENTS.md | 31 ++++++++ README.md | 27 ++++++- pyproject.toml | 22 ++++++ .../__init__.py | 14 ++++ .../adapters/__init__.py | 1 + .../adapters/der_controller.py | 25 +++++++ .../adapters/water_pump_controller.py | 23 ++++++ .../core.py | 59 +++++++++++++++ .../delta.py | 21 ++++++ .../solver.py | 35 +++++++++ test.sh | 7 ++ tests/test_core.py | 71 +++++++++++++++++++ 13 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 pyproject.toml create mode 100644 src/idea171_citypulse_participatory_digital/__init__.py create mode 100644 src/idea171_citypulse_participatory_digital/adapters/__init__.py create mode 100644 src/idea171_citypulse_participatory_digital/adapters/der_controller.py create mode 100644 src/idea171_citypulse_participatory_digital/adapters/water_pump_controller.py create mode 100644 src/idea171_citypulse_participatory_digital/core.py create mode 100644 src/idea171_citypulse_participatory_digital/delta.py create mode 100644 src/idea171_citypulse_participatory_digital/solver.py create mode 100644 test.sh create mode 100644 tests/test_core.py 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..223e1f6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS.md + +Overview +- CityPulse is a cross-domain urban resource optimization platform emphasizing privacy-preserving federated coordination with an offline-first delta-sync protocol. +- This repository contains a production-oriented MVP scaffold in Python that demonstrates core concepts: LocalProblems, SharedSignals, PlanDelta, DualVariables, and an auditable governance trail via AuditLog. + +Architecture (high level) +- Core models (src/idea171_citypulse_participatory_digital/core.py): LocalProblem, SharedSignals, PlanDelta, DualVariables, and AuditLog. +- Delta store and replay (src/idea171_citypulse_participatory_digital/delta.py): deterministic delta history and replay capability. +- Solver (src/idea171_citypulse_participatory_digital/solver.py): a minimal ADMM-lite aggregator over dual variables. +- Adapters (src/idea171_citypulse_participatory_digital/adapters): starter bindings for DER and Water Pump controllers. +- Public APIs are intentionally lightweight for MVP; this repo provides a stable foundation for rapid pilots and extension via adapters and governance. + +Tech Stack +- Language: Python 3.9+ (typing, dataclasses, lightweight architecture) +- Key concepts: LocalProblems (domain tasks), SharedSignals (privacy-preserving signals), PlanDelta (contractual delta actions), DualVariables (shadow prices), AuditLog (tamper-evident-like logging proxy). +- No external services required for MVP; simple in-process delta store and a toy ADMM-like solver to illustrate coordination. + +Development and Testing +- Tests: pytest tests/test_core.py +- Test script: test.sh (runs pytest, then builds the package via python3 -m build) +- Packaging: pyproject.toml with a production-ready package name idea171-citypulse-participatory-digital + +Running locally +- Install dependencies (none beyond stdlib for MVP) +- Run tests: ./test.sh +- Build package: python3 -m build + +Contributing rules +- One change at a time, with tests updated accordingly. +- Ensure tests pass before publishing readiness. diff --git a/README.md b/README.md index ad8a553..5bc1dcb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ -# idea171-citypulse-participatory-digital +# CityPulse: Participatory Digital Twin (Open-Source MVP) -Source logic for Idea #171 \ No newline at end of file +CityPulse aims to be a modular, open-source platform for cross-domain urban resource optimization with privacy-preserving, offline-first federated coordination. This MVP scaffold demonstrates core concepts and provides a path toward production-grade integration with adapters, governance, and a lightweight simulator. + +What’s included +- Core domain models: LocalProblem, SharedSignals, PlanDelta, DualVariables, AuditLog. +- Delta store with deterministic replay for auditable governance trails. +- Minimal ADMM-lite solver to illustrate federated coordination semantics. +- Starter adapters: DERControllerAdapter and WaterPumpControllerAdapter. +- Lightweight packaging setup (pyproject.toml) and test harness. + +How to run +- Run tests and build package: ./test.sh +- The test suite exercises core data structures and adapter bindings. + +Packaging and publishing +- The package is named idea171-citypulse-participatory-digital and is provisioned for publishing to PyPI or a private registry. +- A READY_TO_PUBLISH file will be created when the repository state fully matches publishing requirements. + +Roadmap (high level) +- Phase 0: Skeleton protocol core + adapters over TLS; end-to-end delta-sync. +- Phase 1: Governance ledger scaffolding and identity layer. +- Phase 2: Cross-domain demo in a simulated district. +- Phase 3: Hardware-in-the-loop validation. + +Enjoy contributing and shaping the CityPulse ecosystem. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bcffa70 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools>=61", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "idea171-citypulse-participatory-digital" +version = "0.1.0" +description = "CityPulse: Participatory Digital Twin for Urban Resource Optimization (offline-first federation)" +readme = "README.md" +requires-python = ">=3.9" +license = {file = "LICENSE"} +authors = [ { name = "OpenCode Collaboration" } ] + +[project.urls] +Homepage = "https://example.com/citypulse" + +[tool.setuptools.packages.find] +where = ["src"] +exclude = ["tests*"] + +[tool.setuptools.dynamic] +version = { attr = "__version__" } diff --git a/src/idea171_citypulse_participatory_digital/__init__.py b/src/idea171_citypulse_participatory_digital/__init__.py new file mode 100644 index 0000000..c66ddee --- /dev/null +++ b/src/idea171_citypulse_participatory_digital/__init__.py @@ -0,0 +1,14 @@ +"""CityPulse Participatory Digital Twin package initializer.""" + +__version__ = "0.1.0" + +from .core import LocalProblem, SharedSignals, PlanDelta, DualVariables, AuditLog, AuditLogEntry + +__all__ = [ + "LocalProblem", + "SharedSignals", + "PlanDelta", + "DualVariables", + "AuditLog", + "AuditLogEntry", +] diff --git a/src/idea171_citypulse_participatory_digital/adapters/__init__.py b/src/idea171_citypulse_participatory_digital/adapters/__init__.py new file mode 100644 index 0000000..77e8d97 --- /dev/null +++ b/src/idea171_citypulse_participatory_digital/adapters/__init__.py @@ -0,0 +1 @@ +"""Adapters package for CityPulse MVP.""" diff --git a/src/idea171_citypulse_participatory_digital/adapters/der_controller.py b/src/idea171_citypulse_participatory_digital/adapters/der_controller.py new file mode 100644 index 0000000..0c8f0ee --- /dev/null +++ b/src/idea171_citypulse_participatory_digital/adapters/der_controller.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import Dict, Any, List + + +class DERControllerAdapter: + def __init__(self, adapter_id: str, capabilities: List[str] | None = None) -> None: + self.adapter_id = adapter_id + self.capabilities = capabilities or ["control", "report"] + + def bind(self) -> Dict[str, Any]: + # In a real system this would establish TLS certs, auth, etc. + return { + "adapter_id": self.adapter_id, + "status": "bound", + "capabilities": self.capabilities, + } + + def to_contract(self, data: Dict[str, Any]) -> Dict[str, Any]: + # Minimal canonicalization for demonstration + return { + "contract_id": data.get("contract_id", "unknown"), + "adapter_id": self.adapter_id, + "payload": data, + } diff --git a/src/idea171_citypulse_participatory_digital/adapters/water_pump_controller.py b/src/idea171_citypulse_participatory_digital/adapters/water_pump_controller.py new file mode 100644 index 0000000..42e451a --- /dev/null +++ b/src/idea171_citypulse_participatory_digital/adapters/water_pump_controller.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import Dict, Any, List + + +class WaterPumpControllerAdapter: + def __init__(self, adapter_id: str, capabilities: List[str] | None = None) -> None: + self.adapter_id = adapter_id + self.capabilities = capabilities or ["start", "stop", "status"] + + def bind(self) -> Dict[str, Any]: + return { + "adapter_id": self.adapter_id, + "status": "bound", + "capabilities": self.capabilities, + } + + def to_contract(self, data: Dict[str, Any]) -> Dict[str, Any]: + return { + "contract_id": data.get("contract_id", "unknown"), + "adapter_id": self.adapter_id, + "payload": data, + } diff --git a/src/idea171_citypulse_participatory_digital/core.py b/src/idea171_citypulse_participatory_digital/core.py new file mode 100644 index 0000000..e30c4ce --- /dev/null +++ b/src/idea171_citypulse_participatory_digital/core.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import List, Dict, Any, Optional +from datetime import datetime + + +@dataclass +class LocalProblem: + id: str + district: str + assets: List[str] + objective: str + constraints: Optional[Dict[str, Any]] = None + priority: int = 0 + + +@dataclass +class SharedSignals: + version: int + demand_forecast: Dict[str, float] + weather: Dict[str, Any] + occupancy: Dict[str, float] + priors: Dict[str, float] + privacy_tag: str + + +@dataclass +class PlanDelta: + delta_actions: List[Dict[str, Any]] + timestamp: str + contract_id: str + signer: str + signature: str + + +@dataclass +class DualVariables: + shadow_prices: Dict[str, float] + + +@dataclass +class AuditLogEntry: + entry: str + signer: str + timestamp: str + contract_id: str + version: int + + +class AuditLog: + def __init__(self) -> None: + self.entries: List[AuditLogEntry] = [] + + def append(self, entry: AuditLogEntry) -> None: + self.entries.append(entry) + + def latest(self) -> Optional[AuditLogEntry]: + return self.entries[-1] if self.entries else None diff --git a/src/idea171_citypulse_participatory_digital/delta.py b/src/idea171_citypulse_participatory_digital/delta.py new file mode 100644 index 0000000..526ece2 --- /dev/null +++ b/src/idea171_citypulse_participatory_digital/delta.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List + +from .core import PlanDelta + + +@dataclass +class DeltaStore: + history: List[PlanDelta] + + def __init__(self) -> None: + self.history = [] + + def publish(self, delta: PlanDelta) -> None: + self.history.append(delta) + + def replay(self) -> List[PlanDelta]: + # Deterministic replay: return a shallow copy + return list(self.history) diff --git a/src/idea171_citypulse_participatory_digital/solver.py b/src/idea171_citypulse_participatory_digital/solver.py new file mode 100644 index 0000000..280aeb8 --- /dev/null +++ b/src/idea171_citypulse_participatory_digital/solver.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from typing import List, Dict + +from .delta import DeltaStore +from .core import DualVariables + + +class ADMMLiteSolver: + """A minimal, educational ADMM-lite solver stub. + + This is not a production solver. It provides a deterministic + aggregation of local dual variables to produce a global dual + estimate for the MVP. It exists to demonstrate integration with + the federated coordination idea in CityPulse. + """ + + def __init__(self, participant_count: int) -> None: + self.participant_count = max(1, int(participant_count)) + + def update(self, local_duals: List[DualVariables]) -> DualVariables: + # Simple average of shadow prices across participants + if not local_duals: + return DualVariables(shadow_prices={}) + + keys = set() + for dv in local_duals: + keys.update(dv.shadow_prices.keys()) + + averaged: Dict[str, float] = {} + for k in keys: + total = sum(dv.shadow_prices.get(k, 0.0) for dv in local_duals) + averaged[k] = total / len(local_duals) + + return DualVariables(shadow_prices=averaged) diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..a1a5e08 --- /dev/null +++ b/test.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail +echo "Running tests with pytest..." +pytest -q +echo "Running Python build..." +python3 -m build +echo "All tests passed and package built." diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..8b10867 --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,71 @@ +import json +import os +import sys + +# Ensure the package under src/ is importable when running tests from repo root +ROOT = os.path.dirname(os.path.dirname(__file__)) +SRC = os.path.join(ROOT, "src") +if SRC not in sys.path: + sys.path.insert(0, SRC) + +from idea171_citypulse_participatory_digital.core import LocalProblem, SharedSignals, PlanDelta, DualVariables, AuditLog, AuditLogEntry +from idea171_citypulse_participatory_digital.delta import DeltaStore +from idea171_citypulse_participatory_digital.adapters.der_controller import DERControllerAdapter + + +def test_local_problem_dataclass(): + lp = LocalProblem( + id="lp-001", + district="Downtown", + assets=["building-1", "building-2"], + objective="minimize-peak", + constraints={"max_load": 1000}, + priority=1, + ) + assert lp.id == "lp-001" + assert "Downtown" in lp.district + + +def test_delta_store_and_replay(): + store = DeltaStore() + d1 = PlanDelta( + delta_actions=[{"action": "start"}], + timestamp="2026-04-20T00:00:00Z", + contract_id="c-1", + signer="alice", + signature="sig1", + ) + d2 = PlanDelta( + delta_actions=[{"action": "adjust", "param": "demand"}], + timestamp="2026-04-20T00:05:00Z", + contract_id="c-1", + signer="alice", + signature="sig2", + ) + store.publish(d1) + store.publish(d2) + history = store.replay() + assert len(history) == 2 + assert history[0].timestamp == "2026-04-20T00:00:00Z" + + +def test_audit_log_roundtrip(): + log = AuditLog() + entry = AuditLogEntry( + entry="adapter bound", + signer="system", + timestamp="2026-04-20T00:00:01Z", + contract_id="c-1", + version=1, + ) + log.append(entry) + latest = log.latest() + assert latest is not None and latest.contract_id == "c-1" + + +def test_der_adapter_bind_and_contract(): + der = DERControllerAdapter(adapter_id="der-01") + bound = der.bind() + assert bound["adapter_id"] == "der-01" + contracted = der.to_contract({"contract_id": "c-1", "payload": {"x": 1}}) + assert contracted["adapter_id"] == "der-01"