diff --git a/README.md b/README.md index 77a6d93..15eb5f2 100644 --- a/README.md +++ b/README.md @@ -46,3 +46,34 @@ This repo ships a production-like MVP rather than a toy example. Expect incremen ## Migration and Evolution - Contract migration: The contract registry now supports migrating a contract from an older version to a newer version. This enables adapters to evolve data schemas over time while marking the legacy version as migrated. The old version will have a migrated_to field pointing to the new version, and the new version will register its updated schema. This helps maintain backward-compatibility during MVP-driven evolution of the canonical IR. + +## MVP Roadmap + +- Phase 0: Core interop skeleton (1–2 weeks) + - Solidify Objects/Morphisms/Functors with a versioned ContractRegistry (already scaffolded). + - Ship DSL seeds: LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog, PolicyBlock. + - Implement two starter adapters (rover_planner, habitat_module) connected via TLS, plus a minimal ADMM-lite solver integration. + +- Phase 1: Governance & identity (2–3 weeks) + - Lightweight governance ledger and per-message conformance checks. + - Identity layer (DID or short-lived certs) for adapters and channels. + - Hardened contract conformance tests and deterministic replay on islanding. + +- Phase 2: Cross-domain demo (2–3 weeks) + - Simulated two-agent end-to-end demo with a toy energy/task-allocation objective. + - SDK bindings (Python/C++) for the GoC registry and EnergiBridge-like mappings. + - Publish a minimal, stable GoC registry surface for onboarding adapters. + +- Phase 3: Hardware-in-the-loop (2–3 weeks) + - HIL demo with Gazebo/ROS integration, KPI dashboards for convergence and delta-sync latency. + - Documentation updates and a minimal DSL tutorial for rapid prototyping. + +- Success metrics + - Convergence speed to near-optimal, plan optimality gap vs centralized baseline. + - Delta-sync latency and replay accuracy during islanding/recovery. + - Adapter conformance and overall interoperability scoring. + +- Deliverable artifacts + - DSL sketch docs, toy adapters, EnergiBridge mapping examples, and a publish-ready README. + +If helpful, I can draft concrete toy adapters and a minimal EnergiBridge mapping to accelerate first-to-MVP progress. diff --git a/src/catopt_graph/__init__.py b/src/catopt_graph/__init__.py index 210cefc..16da8e3 100644 --- a/src/catopt_graph/__init__.py +++ b/src/catopt_graph/__init__.py @@ -1,5 +1,6 @@ """CatOpt-Graph MVP package init""" from .core import Object, Morphism, Functor, ContractRegistry +from .dsl import LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog, PolicyBlock, to_json from .admm_lite import AdmmLite __all__ = [ @@ -7,5 +8,13 @@ __all__ = [ "Morphism", "Functor", "ContractRegistry", + "LocalProblem", + "SharedVariables", + "PlanDelta", + "DualVariables", + "PrivacyBudget", + "AuditLog", + "PolicyBlock", + "to_json", "AdmmLite", ] diff --git a/src/catopt_graph/dsl.py b/src/catopt_graph/dsl.py new file mode 100644 index 0000000..e5a1dbe --- /dev/null +++ b/src/catopt_graph/dsl.py @@ -0,0 +1,150 @@ +"""Lightweight DSL primitives for CatOpt-Graph interoperability. + +This module provides minimal, well-typed representations of the canonical +graph-interop primitives used in MVP wiring: +- LocalProblem (Object-like task description) +- SharedVariables (signals, priors, forecasts) +- PlanDelta (incremental plan changes) +- DualVariables (Lagrange multipliers / dual variables) +- PrivacyBudget (privacy budget proxy for governance) +- AuditLog (tamper-evident provenance entries) +- PolicyBlock (safety/exposure controls) + +The goal is to offer a small, framework-agnostic surface that adapters can map +to/from vendor-specific representations via the EnergiBridge-like interop spine. +""" +from __future__ import annotations + +from dataclasses import dataclass, asdict +from typing import Any, Dict, Optional +import json + + +@dataclass +class LocalProblem: + id: str + domain: str + assets: Dict[str, Any] + objective: Dict[str, Any] + constraints: Optional[Dict[str, Any]] = None + solver_hint: Optional[str] = None + + def to_json(self) -> str: + return json.dumps(asdict(self)) + + @staticmethod + def from_json(payload: str) -> "LocalProblem": + data = json.loads(payload) + return LocalProblem(**data) + + +@dataclass +class SharedVariables: + version: str + forecasts: Dict[str, Any] + priors: Optional[Dict[str, Any]] = None + encryption_schema: Optional[str] = None + + def to_json(self) -> str: + return json.dumps(asdict(self)) + + @staticmethod + def from_json(payload: str) -> "SharedVariables": + data = json.loads(payload) + return SharedVariables(**data) + + +@dataclass +class PlanDelta: + delta: Dict[str, Any] + timestamp: float + author: str + contract_id: Optional[str] = None + signature: Optional[str] = None + + def to_json(self) -> str: + return json.dumps(asdict(self)) + + @staticmethod + def from_json(payload: str) -> "PlanDelta": + data = json.loads(payload) + return PlanDelta(**data) + + +@dataclass +class DualVariables: + multipliers: Dict[str, float] + center: Optional[Dict[str, Any]] = None + + def to_json(self) -> str: + return json.dumps(asdict(self)) + + @staticmethod + def from_json(payload: str) -> "DualVariables": + data = json.loads(payload) + return DualVariables(**data) + + +@dataclass +class PrivacyBudget: + signal: str + budget: float + expiry: Optional[str] = None + + def to_json(self) -> str: + return json.dumps(asdict(self)) + + @staticmethod + def from_json(payload: str) -> "PrivacyBudget": + data = json.loads(payload) + return PrivacyBudget(**data) + + +@dataclass +class AuditLog: + entry: str + signer: str + timestamp: float + contract_id: Optional[str] = None + version: Optional[str] = None + + def to_json(self) -> str: + return json.dumps(asdict(self)) + + @staticmethod + def from_json(payload: str) -> "AuditLog": + data = json.loads(payload) + return AuditLog(**data) + + +@dataclass +class PolicyBlock: + safety: Dict[str, Any] + exposure_controls: Optional[Dict[str, Any]] = None + + def to_json(self) -> str: + return json.dumps(asdict(self)) + + @staticmethod + def from_json(payload: str) -> "PolicyBlock": + data = json.loads(payload) + return PolicyBlock(**data) + + +# Simple utility to illustrate interop mapping patterns +def to_json(obj: Any) -> str: + if hasattr(obj, "to_json"): + return obj.to_json() + return json.dumps(asdict(obj)) + + +__all__ = [ + "LocalProblem", + "SharedVariables", + "PlanDelta", + "DualVariables", + "PrivacyBudget", + "AuditLog", + "PolicyBlock", + "to_json", +] diff --git a/tests/test_dsl.py b/tests/test_dsl.py new file mode 100644 index 0000000..ea388e4 --- /dev/null +++ b/tests/test_dsl.py @@ -0,0 +1,39 @@ +import json +from catopt_graph.dsl import LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog, PolicyBlock, to_json + + +def test_local_problem_json_roundtrip(): + lp = LocalProblem( + id="lp-1", + domain="energy", + assets={"battery": 2, "solar": 5}, + objective={"maximize": "util"}, + constraints={"budget": 100}, + solver_hint="fast-convex", + ) + s = lp.to_json() + assert isinstance(s, str) + lp2 = LocalProblem.from_json(s) + assert lp2.id == lp.id + assert lp2.domain == lp.domain + + +def test_shared_variables_json_roundtrip(): + sv = SharedVariables(version="1.0.0", forecasts={"demand": 10}, priors={"mean": 0.0}) + s = sv.to_json() + sv2 = SharedVariables.from_json(s) + assert sv2.version == sv.version + + +def test_to_json_generic(): + lp = LocalProblem( + id="lp-2", + domain="robotics", + assets={"robot": "rover-1"}, + objective={"minimize": "time"}, + constraints=None, + ) + j = to_json(lp) + # ensures a JSON string is produced and decodable + data = json.loads(j) + assert data["id"] == lp.id