build(agent): molt-z#db0ec5 iteration

This commit is contained in:
agent-db0ec53c058f1326 2026-04-15 22:01:58 +02:00
parent d35a3d1d62
commit bf6ea4e05b
6 changed files with 156 additions and 154 deletions

View File

@ -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 - NovaPlan includes a lightweight CatOpt bridge scaffold to map planning primitives
into a canonical representation for interoperability with other runtimes (Open-EnergyMesh, into a canonical representation for interoperability with other runtimes (Open-EnergyMesh,
GridVerse, etc.). 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). - 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. - 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 - See nova_plan/catopt_bridge.py for the core bridge logic and nova_plan/interop_catopt.py for

View File

@ -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)

27
adapters/rover_adapter.py Normal file
View File

@ -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)

View File

@ -1,86 +1,76 @@
"""Lightweight CatOpt bridge scaffold for NovaPlan MVP. """Minimal CatOpt bridge scaffolding for NovaPlan MVP.
Public API used by tests: This module provides tiny, well-scoped helpers to map NovaPlan primitives
- Object: minimal wrapper around a LocalProblem-like object to a canonical CatOpt-like representation suitable for interoperability
- Morphism: wrapper for delta signals between source and destination in MVP experiments.
- 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
""" """
from __future__ import annotations
import json from dataclasses import dataclass
from typing import Any, Dict from typing import Any, Dict, Optional
from time import time
from nova_plan.contracts import PlanDelta from nova_plan.contracts import PlanDelta
from nova_plan.planner import LocalProblem from nova_plan.planner import LocalProblem
class Object: @dataclass
def __init__(self, local_problem: LocalProblem): class ObjectI:
self.local_problem = local_problem """Canonical object representation (per-agent LocalProblem).
@property This is a lightweight wrapper intended for interop exploration.
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.
""" """
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"]

View File

@ -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 This module provides simple, importable helpers to convert between NovaPlan's
CatOpt-compatible pair (Object + Morphism) from a LocalProblem. It leverages LocalProblem representations and the canonical CatOpt bridge objects defined in
the existing bridge utilities and keeps integration concerns centralized. nova_plan.catopt_bridge.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Dict, Any
from nova_plan.dsl import LocalProblemDSL
from nova_plan.planner import LocalProblem 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]: def local_to_canon(local: LocalProblem) -> ObjectI:
"""Convert a LocalProblemDSL description to a CatOpt-style pair. """Convert a LocalProblem to the canonical ObjectI form."""
return to_object(local)
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 to_catopt_pair_from_lp(lp: LocalProblem, source: str, target: str, version: int = 1) -> Dict[str, Any]: def canon_delta_to_morphism(delta: PlanDelta) -> Morphism:
"""Convert an existing LocalProblem instance into a CatOpt-style pair.""" """Convert a PlanDelta into a Morphism as seen by the CatOpt bridge."""
return bridge_pair(lp, source=source, target=target, version=version) return delta_to_morphism(delta)
__all__ = ["local_to_canon", "canon_delta_to_morphism"]

View File

@ -1,66 +1,24 @@
import json import time
import pytest
from nova_plan.dsl import LocalProblemDSL from nova_plan.planner import LocalProblem
from nova_plan.catopt_bridge import Object, Morphism, to_object, to_morphism, bridge_example, validate_contracts 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(): def test_to_object_maps_basic_fields():
# Create a minimal LocalProblem via DSL lp = LocalProblem(id="test-agent", objective=lambda v, s: sum(v.values()) + sum(s.values()), variables={"x": 1.0}, constraints={})
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
obj = to_object(lp) obj = to_object(lp)
od = obj.to_dict() assert isinstance(obj, ObjectI)
assert od["agent_id"] == "rover1" assert obj.id == "test-agent"
assert od["variables"] == {"x": 1.5} assert "variables" in obj.payload
# 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
def test_bridge_example_integration(): def test_delta_to_morphism_maps_delta_fields():
dsl = LocalProblemDSL( delta = {"x": 0.5}
agent_id="habitat1", d = PlanDelta(agent_id="test-agent", delta=delta, timestamp=time.time())
objective_expr='vars["a"] + shared["b"]', m = delta_to_morphism(d, contract_id="c-1", source="test-agent", target="global")
variables={"a": 2.0}, assert isinstance(m, Morphism)
constraints={} assert m.source == "test-agent"
) assert m.target == "global"
lp = dsl.to_local_problem() assert m.data == delta
res = bridge_example(lp, source="habitat1", target="rover1") assert m.contract_id == "c-1"
# 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