build(agent): new-agents-3#dd492b iteration
This commit is contained in:
parent
e3db0d9733
commit
ddf1e643ea
35
README.md
35
README.md
|
|
@ -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
|
Usage
|
||||||
- Privacy by design: secure aggregation, optional local differential privacy, and federated updates.
|
- Install dependencies and run tests with the provided test.sh.
|
||||||
- Distributed optimization core: a robust, ADMM-like solver with convergence guarantees for broad convex classes.
|
- This MVP focuses on correctness and stability for the ADMM-lite loop; cross-domain adapters and governance
|
||||||
- Cross-domain adapters: marketplace and SDK; codegen targets (Rust/C) for edge devices; schema registry for interoperability.
|
layers can be added in future iterations.
|
||||||
- Governance and data policy: auditable logs and policy fragments for visibility control.
|
|
||||||
- Open interoperability: plasma with Open-EnergyMesh and CosmosMesh for cross-domain coordination.
|
|
||||||
|
|
||||||
Getting started
|
This repository is a stepping stone toward the CatOpt-Grid architecture described in AGENTS.md.
|
||||||
- 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
|
|
||||||
|
|
||||||
Contributing
|
Architecture scaffolding: Bridge and Adapters
|
||||||
- See AGENTS.md for architectural rules and contribution guidelines.
|
- 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.
|
||||||
READY_TO_PUBLISH marker is used to signal completion in the publishing workflow.
|
- This scaffolding is intentionally minimal and designed to evolve into a production-grade interop surface without altering core solver behavior.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
from .core import LocalProblem, SharedVariable, PlanDelta, TimeRound
|
"""CatOpt-Grid MVP: category-theoretic compositional optimizer (ADMM-lite).
|
||||||
from .solver import 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"]
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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",
|
||||||
|
]
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools", "wheel"]
|
requires = ["setuptools>=42", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "catopt-grid"
|
name = "catopt-grid"
|
||||||
version = "0.1.0"
|
version = "0.0.1"
|
||||||
description = "Category-theoretic compositional optimizer skeleton for cross-domain distributed edge meshes (MVP)."
|
description = "Category-theoretic compositional optimizer MVP for cross-domain distributed optimization"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.8"
|
||||||
license = {text = "MIT"}
|
dependencies = ["numpy"]
|
||||||
authors = [{name = "OpenCode AI", email = "ops@example.com"}]
|
|
||||||
dependencies = [
|
|
||||||
"numpy>=1.23",
|
|
||||||
"pydantic>=1.10",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["catopt_grid"]
|
where = ["."]
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
Loading…
Reference in New Issue