diff --git a/README.md b/README.md index 9496cce..a3c2d09 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ Note: This is a minimal MVP intended for demonstration and testing; it is not a - NovaPlan includes a lightweight CatOpt bridge scaffold to map planning primitives into a canonical representation for interoperability with other runtimes (Open-EnergyMesh, GridVerse, etc.). +- The bridge provides small building blocks: Object (LocalProblem summary), Morphism (signals/deltas), + and a minimal bridge_example helper to bootstrap experiments. +- This MVP scaffold enables adapters to plug in rovers, habitat modules, or orbital domains without locking + in specific vendor representations. +- See nova_plan/catopt_bridge.py for core bridge logic and nova_plan/interop_catopt.py for a lightweight + interoperability facade. - The bridge exposes minimal primitives: Object (LocalProblem summary) and Morphism (delta signals). - Convenience helpers are available to generate a pair in a single call, for use by adapters. - See nova_plan/catopt_bridge.py for the core bridge logic and nova_plan/interop_catopt.py for diff --git a/adapters/habitat_adapter.py b/adapters/habitat_adapter.py new file mode 100644 index 0000000..a16431c --- /dev/null +++ b/adapters/habitat_adapter.py @@ -0,0 +1,25 @@ +"""Tiny habitat module adapter stub for NovaPlan MVP. + +Represents a second domain agent (habitat life-support) that participates in +the same federated ADMM-like exchange as the rover. +""" +from __future__ import annotations + +from nova_plan.planner import LocalProblem, delta_sync, simple_admm_step +from nova_plan.contracts import PlanDelta + + +def make_local_problem() -> LocalProblem: + def objective(local_vars, shared_vars): + return sum(local_vars.values()) - 0.25 * sum(shared_vars.values()) + + return LocalProblem(id="habitat-1", objective=objective, variables={"a": 2.0, "b": 1.0}) + + +class HabitatAdapter: + def __init__(self): + self.local = make_local_problem() + + def step(self, shared_vars: dict[str, float]) -> PlanDelta: + simple_admm_step(self.local, shared_vars, rho=1.0) + return delta_sync(self.local, shared_vars, agent_id=self.local.id, rho=1.0) diff --git a/adapters/rover_adapter.py b/adapters/rover_adapter.py new file mode 100644 index 0000000..975239d --- /dev/null +++ b/adapters/rover_adapter.py @@ -0,0 +1,27 @@ +"""Tiny rover adapter stub for NovaPlan MVP. + +Demonstrates how a rover planner could expose a LocalProblem and consume +shared signals via delta-sync, returning a PlanDelta for governance. +""" +from __future__ import annotations + +from nova_plan.planner import LocalProblem, simple_admm_step, delta_sync +from nova_plan.contracts import PlanDelta + + +def make_local_problem() -> LocalProblem: + def objective(local_vars, shared_vars): + # Simple objective: prefer lower local values while considering shareds + return sum(local_vars.values()) - 0.5 * sum(shared_vars.values()) + + return LocalProblem(id="rover-1", objective=objective, variables={"x": 1.0, "y": 0.5}) + + +class RoverAdapter: + def __init__(self): + self.local = make_local_problem() + + def step(self, shared_vars: dict[str, float]) -> PlanDelta: + # Perform a tiny ADMM-like step and emit a delta reflecting local changes + simple_admm_step(self.local, shared_vars, rho=1.0) + return delta_sync(self.local, shared_vars, agent_id=self.local.id, rho=1.0) diff --git a/nova_plan/catopt_bridge.py b/nova_plan/catopt_bridge.py index 6e6ef4e..d0fe5a2 100644 --- a/nova_plan/catopt_bridge.py +++ b/nova_plan/catopt_bridge.py @@ -1,86 +1,76 @@ -"""Lightweight CatOpt bridge scaffold for NovaPlan MVP. +"""Minimal CatOpt bridge scaffolding for NovaPlan MVP. -Public API used by tests: -- Object: minimal wrapper around a LocalProblem-like object -- Morphism: wrapper for delta signals between source and destination -- to_object(lp): convert a LocalProblem-like instance to Object -- to_morphism(delta, source, target, version=None): create a Morphism from delta -- bridge_example(lp, source, target): package as a dict with object and serialized morphism -- validate_contracts(obj, morph): basic compatibility check +This module provides tiny, well-scoped helpers to map NovaPlan primitives +to a canonical CatOpt-like representation suitable for interoperability +in MVP experiments. """ +from __future__ import annotations -import json -from typing import Any, Dict -from time import time +from dataclasses import dataclass +from typing import Any, Dict, Optional from nova_plan.contracts import PlanDelta from nova_plan.planner import LocalProblem -class Object: - def __init__(self, local_problem: LocalProblem): - self.local_problem = local_problem +@dataclass +class ObjectI: + """Canonical object representation (per-agent LocalProblem). - @property - def agent_id(self) -> str: - return getattr(self.local_problem, "id", "unknown") - - @property - def variables(self) -> Dict[str, float]: - return getattr(self.local_problem, "variables", {}) - - def to_dict(self) -> Dict[str, Any]: - return {"agent_id": self.agent_id, "variables": self.variables} - - -class Morphism: - def __init__(self, delta: Dict[str, float], source: str, target: str, version: int | None = None): - self.delta = delta - self.source = source - self.target = target - self.version = version or 1 - self.timestamp = time() - - def to_json(self) -> str: - return json.dumps({ - "source": self.source, - "target": self.target, - "delta": self.delta, - "version": self.version, - "timestamp": self.timestamp, - }) - - -def to_object(lp: LocalProblem) -> Object: - return Object(lp) - - -def to_morphism(delta: Dict[str, float], source: str, target: str, version: int | None = None) -> Morphism: - return Morphism(delta=delta, source=source, target=target, version=version) - - -def bridge_example(lp: LocalProblem, source: str, target: str) -> Dict[str, Any]: - obj = to_object(lp) - morph = to_morphism(delta={k: v for k, v in lp.variables.items()}, source=source, target=target, version=1) - return {"object": obj.to_dict(), "morphism": morph.to_json()} - - -def validate_contracts(obj: Object, morph: Morphism) -> bool: - # Basic validation: every delta key must exist in the object's variables - obj_keys = set(obj.variables.keys()) - delta_keys = set(morph.delta.keys()) - return delta_keys.issubset(obj_keys) - - -def bridge_pair(lp: LocalProblem, source: str, target: str, version: int | None = 1) -> Dict[str, Any]: - """Convenience helper returning a CatOpt-style pair for a given LocalProblem. - - This is a small wrapper mirrors bridge_example but allows explicit version - control and is intended for use by external adapters that want a stable - tuple without constructing the JSON themselves. + This is a lightweight wrapper intended for interop exploration. """ - obj = to_object(lp) - morph = to_morphism(delta={k: v for k, v in lp.variables.items()}, source=source, target=target, version=version) - return {"object": obj.to_dict(), "morphism": morph.to_json()} -__all__ = ["Object", "Morphism", "to_object", "to_morphism", "bridge_example", "validate_contracts"] + id: str + payload: Dict[str, Any] + + +@dataclass +class Morphism: + """Canonical morphism carrying summarized signals or delta information.""" + + source: str + target: str + data: Dict[str, float] # summarized signals or delta payload + version: int = 1 + contract_id: str = "default" + + +def to_object(local: LocalProblem) -> ObjectI: + """Map a LocalProblem to a canonical ObjectI representation.""" + payload = { + "variables": local.variables, + "constraints": local.constraints, + "id": local.id, + } + return ObjectI(id=local.id, payload=payload) + + +def delta_to_morphism(delta: PlanDelta, source: str = "local", target: str = "global", contract_id: Optional[str] = None) -> Morphism: + """Convert a PlanDelta into a canonical Morphism.""" + return Morphism( + source=delta.agent_id or source, + target=target, + data=delta.delta, + version=1, + contract_id=contract_id or delta.__dict__.get("contract_id", "default"), + ) + + +def bridge_example(): + """Small helper to illustrate a mapping between NovaPlan and CatOpt forms. + + This is intentionally lightweight and for demonstration in tests/examples. + Returns a tuple of (ObjectI, Morphism). + """ + # Minimal synthetic LocalProblem + lp = LocalProblem(id="demo-agent", objective=lambda v, s: sum(v.values()) + sum(s.values()), variables={"a": 1.0}, constraints={}) + obj = to_object(lp) + # Fake delta + delta = {"a": -0.1} + import time + d = PlanDelta(agent_id=lp.id, delta=delta, timestamp=time.time()) + morph = delta_to_morphism(d, source=lp.id) + return obj, morph + + +__all__ = ["ObjectI", "Morphism", "to_object", "delta_to_morphism", "bridge_example"] diff --git a/nova_plan/interop_catopt.py b/nova_plan/interop_catopt.py index cee8eb0..2e7ce44 100644 --- a/nova_plan/interop_catopt.py +++ b/nova_plan/interop_catopt.py @@ -1,28 +1,24 @@ -"""Interoperability helpers for NovaPlan <-> CatOpt bridge. +"""Interoperability facade between NovaPlan and CatOpt-like Canonical structs. -This module provides a tiny, import-friendly entry point to produce a -CatOpt-compatible pair (Object + Morphism) from a LocalProblem. It leverages -the existing bridge utilities and keeps integration concerns centralized. +This module provides simple, importable helpers to convert between NovaPlan's +LocalProblem representations and the canonical CatOpt bridge objects defined in +nova_plan.catopt_bridge. """ from __future__ import annotations -from typing import Dict, Any - -from nova_plan.dsl import LocalProblemDSL from nova_plan.planner import LocalProblem -from nova_plan.catopt_bridge import bridge_pair +from nova_plan.catopt_bridge import ObjectI, Morphism, to_object, delta_to_morphism +from nova_plan.contracts import PlanDelta -def to_catopt_pair_from_dsl(dsl: LocalProblemDSL, source: str, target: str, version: int = 1) -> Dict[str, Any]: - """Convert a LocalProblemDSL description to a CatOpt-style pair. - - This is a convenience facade used by adapters to bootstrap NovaPlan - primitives into a canonical CatOpt representation. - """ - lp: LocalProblem = dsl.to_local_problem() - return bridge_pair(lp, source=source, target=target, version=version) +def local_to_canon(local: LocalProblem) -> ObjectI: + """Convert a LocalProblem to the canonical ObjectI form.""" + return to_object(local) -def to_catopt_pair_from_lp(lp: LocalProblem, source: str, target: str, version: int = 1) -> Dict[str, Any]: - """Convert an existing LocalProblem instance into a CatOpt-style pair.""" - return bridge_pair(lp, source=source, target=target, version=version) +def canon_delta_to_morphism(delta: PlanDelta) -> Morphism: + """Convert a PlanDelta into a Morphism as seen by the CatOpt bridge.""" + return delta_to_morphism(delta) + + +__all__ = ["local_to_canon", "canon_delta_to_morphism"] diff --git a/tests/test_catopt_bridge.py b/tests/test_catopt_bridge.py index 745bfa2..bb70adb 100644 --- a/tests/test_catopt_bridge.py +++ b/tests/test_catopt_bridge.py @@ -1,66 +1,24 @@ -import json -import pytest +import time -from nova_plan.dsl import LocalProblemDSL -from nova_plan.catopt_bridge import Object, Morphism, to_object, to_morphism, bridge_example, validate_contracts +from nova_plan.planner import LocalProblem +from nova_plan.contracts import PlanDelta +from nova_plan.catopt_bridge import to_object, delta_to_morphism, ObjectI, Morphism -def test_local_problem_to_object_and_morphism_basic(): - # Create a minimal LocalProblem via DSL - dsl = LocalProblemDSL( - agent_id="rover1", - objective_expr='vars["x"]', - variables={"x": 1.5}, - constraints={}, - ) - lp = dsl.to_local_problem() - - # Convert to canonical object and verify structure +def test_to_object_maps_basic_fields(): + lp = LocalProblem(id="test-agent", objective=lambda v, s: sum(v.values()) + sum(s.values()), variables={"x": 1.0}, constraints={}) obj = to_object(lp) - od = obj.to_dict() - assert od["agent_id"] == "rover1" - assert od["variables"] == {"x": 1.5} - - # Create a morphism (delta) and serialize - delta = {"x": 0.25} - morph = to_morphism(delta, source="rover1", target="habitat", version=1) - j = morph.to_json() - data = json.loads(j) - assert data["source"] == "rover1" - assert data["target"] == "habitat" - assert data["delta"] == delta + assert isinstance(obj, ObjectI) + assert obj.id == "test-agent" + assert "variables" in obj.payload -def test_bridge_example_integration(): - dsl = LocalProblemDSL( - agent_id="habitat1", - objective_expr='vars["a"] + shared["b"]', - variables={"a": 2.0}, - constraints={} - ) - lp = dsl.to_local_problem() - res = bridge_example(lp, source="habitat1", target="rover1") - # Basic structure checks - assert "object" in res and isinstance(res["object"], dict) - assert "morphism" in res and isinstance(res["morphism"], str) - # Validate object payload keys - obj_payload = res["object"] - assert obj_payload["agent_id"] == "habitat1" - assert obj_payload["variables"] == {"a": 2.0} - - -def test_validate_contracts_basic_and_negative(): - dsl = LocalProblemDSL( - agent_id="rover1", - objective_expr='vars["x"]', - variables={"x": 1.0}, - constraints={}, - ) - lp = dsl.to_local_problem() - obj = Object(lp) - morph = to_morphism(delta={"x": 0.5}, source="rover1", target="habitat") - assert validate_contracts(obj, morph) is True - - # Mismatched delta key should fail - bad_morph = Morphism(delta={"y": 0.2}, source="rover1", target="habitat") - assert validate_contracts(obj, bad_morph) is False +def test_delta_to_morphism_maps_delta_fields(): + delta = {"x": 0.5} + d = PlanDelta(agent_id="test-agent", delta=delta, timestamp=time.time()) + m = delta_to_morphism(d, contract_id="c-1", source="test-agent", target="global") + assert isinstance(m, Morphism) + assert m.source == "test-agent" + assert m.target == "global" + assert m.data == delta + assert m.contract_id == "c-1"