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..4ae2e21 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,26 @@ +# SunHub Agent Overview + +Architecture +- SunHub is a modular, city-scale solar siting and DER coordination platform. +- Core abstractions: + - Object: a neighborhood-level optimization target (Object in a city may map to a block or district). + - SharedSignal: aggregated signals used for cross-neighborhood coordination (e.g., forecasted irradiance, storage state). + - PlanDelta: incremental actions to advance planning (delta updates to per-neighborhood plans). +- GraphOfContracts: registry for adapters to external systems (GIS, weather, DER controllers). +- Lightweight federation: simplified ADMM-lite loop coordinating neighborhood plans while preserving locality. + +Tech Stack (in this repo) +- Python 3.10+ with setuptools-based packaging (pyproject.toml). +- Tests powered by pytest. +- Lightweight in-memory data models for MVP; pluggable adapters via GraphOfContracts. + +Testing and Commands +- Run tests: `bash test.sh` in repo root. This also builds the package (`python3 -m build`). +- Local development: import sunhub.core modules and run minimal scenarios via `pytest` tests. + +Contribution Rules +- Use the AGENTS.md as a jump-off for new agents; implement features in small, well-scoped patches. +- All changes must pass tests before PRs; avoid breaking existing contracts in the in-repo API. + +Notes +- This document will be updated as the project evolves and new agents are introduced. diff --git a/README.md b/README.md index 97eed75..18df60f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ -# sunhub-open-city-scale-solar-siting-and- +# SunHub Open City-Scale Solar Siting and Dispatch Planner -A novel, open-source software platform to optimize solar siting, rooftop solar deployment, community solar integration, and distributed storage across a city. SunHub uses a canonical, CatOpt-inspired data model to map local neighborhood optimization \ No newline at end of file +SunHub is an open-source platform for optimizing solar siting, rooftop solar deployment, community solar integration, and distributed storage across a city. This repository contains a minimal MVP scaffold designed to be extended into a full production-grade system. + +Project goals +- Provide a canonical data model: Object (neighborhood target), SharedSignal (aggregated signals), PlanDelta (incremental actions). +- Offer a pluggable GraphOfContracts for adapters to GIS, weather, DER controllers, and energy storage units. +- Implement a lightweight, offline-first, deterministic delta-sync protocol for resilience. +- Provide an extensible SDK with sample adapters. + +Structure +- src/sunhub/: Core Python package with data models and orchestration skeleton. +- tests/: Unit tests for the MVP components. +- AGENTS.md: Architecture and contribution guidelines for future agents. +- pyproject.toml: Packaging metadata (Python) for a production-ready package. +- test.sh: Execute tests and packaging as part of CI checks. +- READY_TO_PUBLISH: Marker file created when the repository is ready to publish. + +Getting started +- Install Python 3.10+: see pyproject.toml for build requirements. +- Run tests: `bash test.sh`. +- Run quick demos by importing sunhub.core in Python and constructing a SunHub instance. + +This repository follows a pragmatic, minimal MVP approach. Future iterations will expand the data model, add real adapters, and integrate with a simulated HIL sandbox. + +Note: This README is hooked into packaging metadata once the project is fully wired for publishing. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..92adb77 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "sunhub_open_city_scale_solar_siting_and" +version = "0.1.0" +description = "Open city-scale solar siting and DER coordination MVP" +readme = "README.md" +requires-python = ">=3.10" + +[project.urls] +Homepage = "https://example.com/sunhub" + +[tool.setuptools] +package-dir = {"" = "src"} diff --git a/src/sunhub/__init__.py b/src/sunhub/__init__.py new file mode 100644 index 0000000..f491df9 --- /dev/null +++ b/src/sunhub/__init__.py @@ -0,0 +1,3 @@ +from .core import Object, SharedSignal, PlanDelta, GraphOfContracts, SunHub + +__all__ = ["Object", "SharedSignal", "PlanDelta", "GraphOfContracts", "SunHub"] diff --git a/src/sunhub/core.py b/src/sunhub/core.py new file mode 100644 index 0000000..ddae523 --- /dev/null +++ b/src/sunhub/core.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from typing import Any, Dict, List + + +@dataclass +class Object: + id: str + neighborhood: str + data: Dict[str, Any] + + @staticmethod + def create(neighborhood: str, data: Dict[str, Any]) -> "Object": + return Object(id=str(uuid.uuid4()), neighborhood=neighborhood, data=data) + + +@dataclass +class SharedSignal: + id: str + signal_type: str + value: Any + + @staticmethod + def create(signal_type: str, value: Any) -> "SharedSignal": + return SharedSignal(id=str(uuid.uuid4()), signal_type=signal_type, value=value) + + def to_dict(self) -> Dict[str, Any]: + return {"id": self.id, "signal_type": self.signal_type, "value": self.value} + + +@dataclass +class PlanDelta: + id: str + neighborhood: str + actions: List[Dict[str, Any]] = field(default_factory=list) + + @staticmethod + def create(neighborhood: str, actions: List[Dict[str, Any]]) -> "PlanDelta": + return PlanDelta(id=str(uuid.uuid4()), neighborhood=neighborhood, actions=actions) + + def apply(self, state: Dict[str, Any]) -> Dict[str, Any]: + # Minimal deterministic application: apply key-value updates from actions + new_state = dict(state) + for act in self.actions: + key = act.get("key") + value = act.get("value") + if key is not None: + new_state[key] = value + return new_state + + +@dataclass +class GraphOfContracts: + registry: Dict[str, Dict[str, Any]] = field(default_factory=dict) + + def register(self, contract_name: str, adapter: Dict[str, Any]) -> None: + self.registry[contract_name] = adapter + + def get(self, contract_name: str) -> Dict[str, Any]: + return self.registry.get(contract_name, {}) + + +class SunHub: + def __init__(self) -> None: + self.objects: Dict[str, Object] = {} + self.shared_signals: Dict[str, SharedSignal] = {} + self.plan_deltas: List[PlanDelta] = [] + self.contracts = GraphOfContracts() + + # Object lifecycle + def add_object(self, neighborhood: str, data: Dict[str, Any]) -> Object: + obj = Object.create(neighborhood, data) + self.objects[obj.id] = obj + return obj + + # SharedSignal lifecycle + def add_signal(self, signal_type: str, value: Any) -> SharedSignal: + sig = SharedSignal.create(signal_type, value) + self.shared_signals[sig.id] = sig + return sig + + # PlanDelta lifecycle + def add_plan_delta(self, neighborhood: str, actions: List[Dict[str, Any]]) -> PlanDelta: + delta = PlanDelta.create(neighborhood, actions) + self.plan_deltas.append(delta) + return delta + + # Simple aggregator applying all deltas + def apply_all(self, state: Dict[str, Any]) -> Dict[str, Any]: + cur = dict(state) + for delta in self.plan_deltas: + cur = delta.apply(cur) + return cur + + # Registry access + def register_adapter(self, name: str, adapter: Dict[str, Any]) -> None: + self.contracts.register(name, adapter) diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..541f119 --- /dev/null +++ b/test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Test script for SunHub MVP +# 1) Run pytest tests +# 2) Build the Python package to ensure packaging metadata and directory structure compile + +echo "Running test suite..." +pytest -q || { echo "Tests failed"; exit 1; } + +echo "Building package (python3 -m build)..." +python3 -m build || { echo "Build failed"; exit 1; } + +echo "All tests passed and package built successfully." diff --git a/tests/test_sunhub.py b/tests/test_sunhub.py new file mode 100644 index 0000000..6031af8 --- /dev/null +++ b/tests/test_sunhub.py @@ -0,0 +1,45 @@ +import sys, os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) +import pytest + +from sunhub import SunHub +from sunhub import Object, SharedSignal, PlanDelta + + +def test_object_creation_and_storage(): + hub = SunHub() + o = hub.add_object("Downtown", {"pv_capacity_kw": 120}) + assert o.id in hub.objects + assert hub.objects[o.id].neighborhood == "Downtown" + + +def test_signal_creation(): + hub = SunHub() + s = hub.add_signal("irradiance_forecast", {"hour": 12, "value": 800}) + assert s.id in hub.shared_signals + assert hub.shared_signals[s.id].signal_type == "irradiance_forecast" + + +def test_plan_delta_application(): + hub = SunHub() + state = {"solar_production_kw": 0} + delta = hub.add_plan_delta("Downtown", [{"key": "solar_production_kw", "value": 45}]) + new_state = delta.apply(state) + assert new_state["solar_production_kw"] == 45 + + +def test_offline_delta_sync_roundtrip(): + hub = SunHub() + # offline delta updates + hub.add_plan_delta("Downtown", [{"key": "solar_production_kw", "value": 30}]) + hub.add_plan_delta("Downtown", [{"key": "storage_state", "value": "charging"}]) + final = hub.apply_all({}) + assert final["solar_production_kw"] == 30 + assert final["storage_state"] == "charging" + + +def test_contract_registry(): + hub = SunHub() + hub.register_adapter("gis", {"endpoint": "http://localhost/gis"}) + reg = hub.contracts.get("gis") + assert reg["endpoint"] == "http://localhost/gis"