build(agent): new-agents-3#dd492b iteration

This commit is contained in:
agent-dd492b85242a98c5 2026-04-19 22:09:44 +02:00
parent e3db0d9733
commit ddf1e643ea
11 changed files with 354 additions and 31 deletions

View File

@ -1,22 +1,23 @@
# CatOpt-Grid
# CatOpt-Grid MVP
Category-Theoretic Compositional Optimizer for Cross-Domain, Privacy-Preserving Distributed Edge Meshes.
A production-friendly MVP for a category-theoretic, cross-domain distributed optimization
framework. This repository implements the core primitives and a tiny ADMM-lite solver to
help validate the architecture and provide a concrete starting point for adapters and cross-domain
integration.
This repository provides a production-ready skeleton for CatOpt-Grid, a framework that expresses distributed optimization problems across heterogeneous edge devices (DERs, meters, mobility chargers, water pumps) using category-theoretic primitives: Objects (local problems), Morphisms (data exchange channels), and Functors (adapters). It aims to enable composability, privacy-preserving aggregation, and delta-sync semantics across partitions.
Key components
- LocalProblem: per-asset optimization task with a convex quadratic objective and bound constraints.
- SharedVariable: consensus variable used by all agents in the ADMM-like loop.
- ADMMLiteSolver: lightweight solver implementing x-update, z-update, and dual variable updates with bound projection.
Design goals
- Privacy by design: secure aggregation, optional local differential privacy, and federated updates.
- Distributed optimization core: a robust, ADMM-like solver with convergence guarantees for broad convex classes.
- Cross-domain adapters: marketplace and SDK; codegen targets (Rust/C) for edge devices; schema registry for interoperability.
- Governance and data policy: auditable logs and policy fragments for visibility control.
- Open interoperability: plasma with Open-EnergyMesh and CosmosMesh for cross-domain coordination.
Usage
- Install dependencies and run tests with the provided test.sh.
- This MVP focuses on correctness and stability for the ADMM-lite loop; cross-domain adapters and governance
layers can be added in future iterations.
Getting started
- This is a skeleton MVP focused on core primitives and a minimal solver to enable testing and integration.
- Install: python3 -m pip install . (after packaging)
- Run tests: bash test.sh
This repository is a stepping stone toward the CatOpt-Grid architecture described in AGENTS.md.
Contributing
- See AGENTS.md for architectural rules and contribution guidelines.
READY_TO_PUBLISH marker is used to signal completion in the publishing workflow.
Architecture scaffolding: Bridge and Adapters
- catopt_grid.bridge: lightweight interoperability layer with IRObject/IRMorphism and a tiny GraphOfContracts registry to version adapters and data schemas.
- catopt_grid.adapters: starter adapters (rover_planner, habitat_module) that illustrate mapping local problems to the canonical IR and seed cross-domain interoperability.
- This scaffolding is intentionally minimal and designed to evolve into a production-grade interop surface without altering core solver behavior.

View File

@ -1,4 +1,9 @@
from .core import LocalProblem, SharedVariable, PlanDelta, TimeRound
from .solver import admm_lite
"""CatOpt-Grid MVP: category-theoretic compositional optimizer (ADMM-lite).
__all__ = ["LocalProblem", "SharedVariable", "PlanDelta", "TimeRound", "admm_lite"]
This package provides a tiny, production-friendly core for testing the
distributed-optimization primitives inspired by the CatOpt-Grid vision.
"""
from .admm_lite import LocalProblem, SharedVariable, ADMMLiteSolver
__all__ = ["LocalProblem", "SharedVariable", "ADMMLiteSolver"]

View File

@ -0,0 +1,11 @@
"""Adapters package for CatOpt-Grid interoperability (toy seeds).
This module exposes two starter adapters: rover_planner and habitat_module.
They provide minimal scaffolding to illustrate how device-specific problems map
to the canonical IR defined in catopt_grid.bridge.
"""
from .rover_planner import RoverPlannerAdapter
from .habitat_module import HabitatModuleAdapter
__all__ = ["RoverPlannerAdapter", "HabitatModuleAdapter"]

View File

@ -0,0 +1,31 @@
"""Base class for adapters mapping local problems to canonical IR."""
from __future__ import annotations
from typing import Dict, Optional
from catopt_grid.bridge import IRObject, IRMorphism, PlanDelta
from catopt_grid.adapters import __version__ as _ADAPTER_VERSION # type: ignore
class AdapterBase:
"""Minimal adapter base class.
Subclasses should implement to_ir_object and to_morphism mappings.
"""
name: str
version: str = "0.0.1"
def __init__(self, name: str) -> None:
self.name = name
def to_ir_object(self, local_problem) -> Optional[IRObject]: # pragma: no cover
"""Convert a local problem to canonical IRObject. Override in subclasses."""
return None
def to_morphism(self, data) -> Optional[IRMorphism]: # pragma: no cover
"""Convert data to a canonical IRMorphism. Override in subclasses."""
return None
__all__ = ["AdapterBase"]

View File

@ -0,0 +1,20 @@
"""Toy Habitat Module Adapter.
Provides a minimal interface to map habitat/local-grid problems to the IR.
"""
from __future__ import annotations
from catopt_grid.bridge import IRObject
from .base import AdapterBase
class HabitatModuleAdapter(AdapterBase):
def __init__(self, name: str = "habitat_module") -> None:
super().__init__(name)
def to_ir_object(self, local_problem) -> IRObject:
# Basic heuristic mapping; richer metadata can be added later
return IRObject(id=getattr(local_problem, "name", "habitat"), dimension=int(getattr(local_problem, "n", 1)), metadata={"source": self.name})
__all__ = ["HabitatModuleAdapter"]

View File

@ -0,0 +1,30 @@
"""Toy Rover Planner Adapter.
Maps a rover-local planning problem to the canonical IRObject and uses a
simple local solution strategy. This is intentionally minimal to seed
interoperability and can be evolved into a full adapter later.
"""
from __future__ import annotations
from typing import Optional
from catopt_grid.bridge import IRObject, GraphOfContracts
from .base import AdapterBase
class RoverPlannerAdapter(AdapterBase):
def __init__(self, name: str = "rover_planner") -> None:
super().__init__(name)
self._goc = GraphOfContracts()
def to_ir_object(self, local_problem) -> Optional[IRObject]:
# Minimal mapping: copy dimension as the IR object's dimension.
if getattr(local_problem, "n", None) is None:
return None
return IRObject(id=getattr(local_problem, "name", "rover"), dimension=int(local_problem.n), metadata={"source": self.name})
def contract_registry(self) -> GraphOfContracts:
return self._goc
__all__ = ["RoverPlannerAdapter"]

104
catopt_grid/admm_lite.py Normal file
View File

@ -0,0 +1,104 @@
import numpy as _np
from typing import List, Tuple, Optional
class LocalProblem:
"""A minimal local optimization problem of the form:
min_x 0.5 x^T Q x + b^T x subject to lb <= x <= ub
which is convex if Q is positive semidefinite.
"""
def __init__(
self,
n: int,
Q: _np.ndarray,
b: _np.ndarray,
lb: Optional[_np.ndarray] = None,
ub: Optional[_np.ndarray] = None,
name: Optional[str] = None,
) -> None:
self.n = int(n)
self.Q = _np.asarray(Q).reshape((self.n, self.n))
self.b = _np.asarray(b).reshape((self.n,))
self.lb = _np.asarray(lb).reshape((self.n,)) if lb is not None else _np.full((self.n,), -_np.inf)
self.ub = _np.asarray(ub).reshape((self.n,)) if ub is not None else _np.full((self.n,), _np.inf)
self.name = name or f"LocalProblem_{self.n}d"
def __repr__(self) -> str:
return f"LocalProblem(name={self.name}, n={self.n})"
class SharedVariable:
"""Represents a global/shared variable (consensus variable) across agents."""
def __init__(self, dim: int):
self.dim = int(dim)
self.value = _np.zeros(self.dim)
def __repr__(self) -> str:
return f"SharedVariable(dim={self.dim})"
class ADMMLiteSolver:
"""A lightweight, ADMM-like solver for separable quadratic problems.
Assumes all LocalProblem instances share the same x dimension. Each agent
solves its own quadratic subproblem with a quadratic penalty coupling term
to the global consensus z. The updates are:
x_i = (Q_i + rho I)^{-1} [ rho*(z - u_i) - b_i ]
z = average_i ( x_i + u_i )
u_i = u_i + x_i - z
and then x_i is clipped to [lb_i, ub_i].
"""
def __init__(self, problems: List[LocalProblem], rho: float = 1.0, max_iter: int = 100, tol: float = 1e-4):
if not problems:
raise ValueError("ADMMLiteSolver requires at least one LocalProblem")
self.problems = problems
self.rho = float(rho)
self.max_iter = int(max_iter)
self.tol = float(tol)
# Validate dimensions and prepare internal structures
self._validate_dimensions()
def _validate_dimensions(self) -> None:
n0 = self.problems[0].n
for p in self.problems:
if p.n != n0:
raise ValueError("All LocalProblem instances must have the same dimension n")
self.n = n0
def solve(self, initial_z: _np.ndarray | None = None) -> Tuple[_np.ndarray, List[_np.ndarray], List[_np.ndarray]]:
N = len(self.problems)
if initial_z is None:
z = _np.zeros(self.n)
else:
z = _np.asarray(initial_z).reshape((self.n,))
us: List[_np.ndarray] = [_np.zeros(self.n) for _ in range(N)]
xs: List[_np.ndarray] = [_np.zeros(self.n) for _ in range(N)]
# Precompute cholesky-like solve factors if Q is diagonalizable; for general Q we'll use numpy.solve
for it in range(self.max_iter):
max_diff = 0.0
# x-update per agent
for i, lp in enumerate(self.problems):
M = lp.Q + self.rho * _np.eye(self.n)
rhs = self.rho * (z - us[i]) - lp.b
xi = _np.linalg.solve(M, rhs)
# apply simple bound projection
xi = _np.minimum(_np.maximum(xi, lp.lb), lp.ub)
xs[i] = xi
diff = _np.linalg.norm(xi - z)
if diff > max_diff:
max_diff = diff
# z-update (consensus)
z_new = sum((xs[i] + us[i]) for i in range(N)) / N
# dual update
for i in range(N):
us[i] = us[i] + xs[i] - z_new
z = z_new
if max_diff < self.tol:
break
return z, xs, us

90
catopt_grid/bridge.py Normal file
View File

@ -0,0 +1,90 @@
"""CatOpt-Grid Bridge: Canonical Interoperability Layer (minimal).
This module provides a small, production-friendly scaffold to map the
CatOpt-Grid primitives to a vendor-agnostic intermediate representation
(IR). It is intentionally lightweight and intended as a starting point for the
MVP wiring described in the community plan.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Optional
import time
@dataclass
class PrivacyBudget:
budget: float
unit: str = "epsilon"
@dataclass
class AuditLogEntry:
message: str
timestamp: float = field(default_factory=lambda: time.time())
signature: Optional[str] = None
@dataclass
class PlanDelta:
delta: List[float]
version: int
timestamp: float = field(default_factory=lambda: time.time())
@dataclass
class IRObject:
"""Canonical Object: a local optimization problem (per-asset task).
This is intentionally aligned with LocalProblem from catopt_grid.core/admm_lite
to ease adoption. Users can extend the IR with extra metadata as needed.
"""
id: str
dimension: int
# Optional per-object metadata
metadata: Dict[str, object] = field(default_factory=dict)
@dataclass
class IRMorphism:
"""Canonical Morphism: a channel carrying shared data among agents."""
name: str
value: List[float]
version: int = 0
class GraphOfContracts:
"""Lightweight registry mapping adapters to their contract metadata.
This is a very small stand-in for a more feature-rich contract registry
(GoC) that would live in a real deployment. It records versions and simple
schemas for cross-domain interoperability.
"""
def __init__(self) -> None:
self._contracts: Dict[str, Dict[str, object]] = {}
def register(self, adapter_name: str, contract: Dict[str, object]) -> None:
self._contracts[adapter_name] = {
"contract": contract,
"version": contract.get("version", 1),
"timestamp": time.time(),
}
def get_contract(self, adapter_name: str) -> Optional[Dict[str, object]]:
return self._contracts.get(adapter_name, None)
def list_contracts(self) -> List[Dict[str, object]]:
items = []
for name, meta in self._contracts.items():
items.append({"adapter": name, **meta})
return sorted(items, key=lambda x: x.get("timestamp", 0))
__all__ = [
"PrivacyBudget",
"AuditLogEntry",
"PlanDelta",
"IRObject",
"IRMorphism",
"GraphOfContracts",
]

8
conftest.py Normal file
View File

@ -0,0 +1,8 @@
import sys
import os
# Ensure the repository root is on Python path so tests can import the package
# even if pytest is invoked from a subdirectory or with an altered PYTHONPATH.
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
if ROOT_DIR not in sys.path:
sys.path.insert(0, ROOT_DIR)

View File

@ -1,19 +1,14 @@
[build-system]
requires = ["setuptools", "wheel"]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "catopt-grid"
version = "0.1.0"
description = "Category-theoretic compositional optimizer skeleton for cross-domain distributed edge meshes (MVP)."
version = "0.0.1"
description = "Category-theoretic compositional optimizer MVP for cross-domain distributed optimization"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "MIT"}
authors = [{name = "OpenCode AI", email = "ops@example.com"}]
dependencies = [
"numpy>=1.23",
"pydantic>=1.10",
]
requires-python = ">=3.8"
dependencies = ["numpy"]
[tool.setuptools.packages.find]
where = ["catopt_grid"]
where = ["."]

28
tests/test_admm.py Normal file
View File

@ -0,0 +1,28 @@
import numpy as _np
from catopt_grid import LocalProblem, ADMMLiteSolver
def test_admm_lite_two_agents_quadratic_convergence():
# Two 1-D local problems with simple quadratics.
# f1(x) = 0.5 * 2 * x^2 + (-6) * x => Q1=2, b1=-6, bounds [0, 5]
Q1 = _np.array([[2.0]])
b1 = _np.array([-6.0])
lp1 = LocalProblem(n=1, Q=Q1, b=b1, lb=_np.array([0.0]), ub=_np.array([5.0]), name="lp1")
# f2(x) = 0.5 * 1 * x^2 + (-2) * x => Q2=1, b2=-2, bounds [0, 5]
Q2 = _np.array([[1.0]])
b2 = _np.array([-2.0])
lp2 = LocalProblem(n=1, Q=Q2, b=b2, lb=_np.array([0.0]), ub=_np.array([5.0]), name="lp2")
solver = ADMMLiteSolver([lp1, lp2], rho=1.0, max_iter=1000, tol=1e-6)
z, xs, us = solver.solve()
# Check that the consensus value is within bounds and that x_i are close to each other
assert z.shape == (1,)
assert all(0.0 <= xi[0] <= 5.0 for xi in xs)
# Since both x_i converge to the same z, their difference should be small
diffs = [_np.linalg.norm(xs[i] - xs[0]) for i in range(1, len(xs))]
assert max(diffs) < 1e-4
# And u_i should be finite
for ui in us:
assert _np.all(_np.isfinite(ui))