diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd5590b --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +node_modules/ +.npmrc +.env +.env.* +__tests__/ +coverage/ +.nyc_output/ +dist/ +build/ +.cache/ +*.log +.DS_Store +tmp/ +.tmp/ +__pycache__/ +*.pyc +.venv/ +venv/ +*.egg-info/ +.pytest_cache/ +READY_TO_PUBLISH diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..74b9b99 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +Repository: CatOpt-Play (prototype) + +Architecture +- Language: Python 3.8+ +- Layout: src/ package layout (src/idea36_catopt_play_category) +- Core components: + - contracts: pydantic models for LocalProblem, SharedVariables, DualVariables, PlanDelta, PrivacyBudget, AuditLog + - solver: an ADMM-lite consensus solver (prototype) + +Tech stack +- Python with pydantic for data contracts +- pytest for tests +- setuptools/pyproject for packaging + +Testing & Commands +- Run tests: `pytest` +- Build package: `python3 -m build` +- Full automation: `bash test.sh` (installs build+pytest in the environment, installs package editable, runs tests, then builds) + +Rules for AI agents and contributors +- Make minimal, well-scoped edits. Prefer small changes. +- Follow src/ layout and put package code under `src/idea36_catopt_play_category`. +- Add tests for new behaviour. CI expects `pytest` to pass. +- Do not create READY_TO_PUBLISH unless the full original spec is implemented and tests pass. diff --git a/README.md b/README.md index 5a573e1..5748abf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ -# idea36-catopt-play-category +# CatOpt-Play (prototype) -Source logic for Idea #36 \ No newline at end of file +This repository contains a Python prototype for CatOpt-Play — a category-theory-inspired compositional optimizer for distributed multi-agent coordination. The goal of this prototype is to provide a canonical IR for local problems and data contracts, plus a small ADMM-lite solver demonstrating distributed consensus and delta-style plan deltas. + +Contents +- src/idea36_catopt_play_category: core library (contracts, solver) +- tests: basic tests for solver convergence and schema generation +- AGENTS.md: repository architecture and contribution rules +- test.sh: runs tests and builds the package + +Quickstart +1. Install dev tools: `pip install -U build pytest` +2. Install package in editable mode: `pip install -e .` +3. Run tests: `pytest` +4. Build distribution: `python3 -m build` + +This prototype focuses on a small, well-tested chunk: the canonical data contracts and an ADMM-lite consensus solver. It is intentionally minimal and designed to be extended with engine adapters, transports, and governance ledgers in follow-up work. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b61373e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..924c4a7 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,16 @@ +[metadata] +name = idea36-catopt-play-category +version = 0.1.0 +description = CatOpt-Play: Category-Theoretic Compositional Optimizer (prototype) +long_description = file: README.md +long_description_content_type = text/markdown +author = OpenCode +license = MIT + +[options] +package_dir = + = src +packages = find: +python_requires = >=3.8 +install_requires = + pydantic>=1.10 diff --git a/src/idea36_catopt_play_category/__init__.py b/src/idea36_catopt_play_category/__init__.py new file mode 100644 index 0000000..1d06289 --- /dev/null +++ b/src/idea36_catopt_play_category/__init__.py @@ -0,0 +1,4 @@ +"""CatOpt-Play prototype package.""" +from . import contracts, solver + +__all__ = ["contracts", "solver"] diff --git a/src/idea36_catopt_play_category/contracts.py b/src/idea36_catopt_play_category/contracts.py new file mode 100644 index 0000000..89d56b6 --- /dev/null +++ b/src/idea36_catopt_play_category/contracts.py @@ -0,0 +1,57 @@ +from typing import Dict, Any, List, Optional +from pydantic import BaseModel, Field +from datetime import datetime + + +class LocalProblem(BaseModel): + """Canonical representation of a local agent planning problem. + + For the prototype we model simple quadratic objectives with coefficients + so the ADMM updates can be computed analytically in tests. + """ + + id: str + # objective: 0.5 * a * x^2 + b * x + a: float = Field(..., description="Quadratic coefficient (>=0)") + b: float = Field(..., description="Linear coefficient") + constraints: Optional[Dict[str, Any]] = None + + +class SharedVariables(BaseModel): + values: Dict[str, float] + version: str + timestamp: datetime + + +class DualVariables(BaseModel): + values: Dict[str, float] + version: str + timestamp: datetime + + +class PlanDelta(BaseModel): + agent_id: str + delta: Dict[str, float] + version: str + timestamp: datetime + nonce: Optional[str] = None + + +class PrivacyBudget(BaseModel): + remaining: float + used: float = 0.0 + budget_id: Optional[str] = None + + +class AuditLog(BaseModel): + event_id: str + actor: str + action: str + details: Optional[Dict[str, Any]] = None + timestamp: datetime + + +def export_json_schemas() -> Dict[str, Any]: + """Return JSON schemas for canonical contracts.""" + models = [LocalProblem, SharedVariables, DualVariables, PlanDelta, PrivacyBudget, AuditLog] + return {m.__name__: m.schema() for m in models} diff --git a/src/idea36_catopt_play_category/solver.py b/src/idea36_catopt_play_category/solver.py new file mode 100644 index 0000000..d824413 --- /dev/null +++ b/src/idea36_catopt_play_category/solver.py @@ -0,0 +1,46 @@ +from typing import List, Dict +import math + + +def admm_consensus(local_problems: List[Dict[str, float]], rho: float = 1.0, max_iter: int = 200, tol: float = 1e-4): + """ + Simple ADMM consensus solver for scalar variables. + + local_problems: list of dicts with keys 'a' and 'b' representing local objective + 0.5 * a * x^2 + b * x + + Returns tuple (z, history) where z is consensus variable and history a list of z over iterations. + """ + n = len(local_problems) + # initialize + x = [0.0 for _ in range(n)] + u = [0.0 for _ in range(n)] + z = 0.0 + history = [] + + for k in range(max_iter): + # x-update (closed-form for quadratic) + for i, p in enumerate(local_problems): + a = p.get("a", 0.0) + b = p.get("b", 0.0) + denom = a + rho + # minimize 0.5*a*x^2 + b*x + (rho/2)*(x - z + u)^2 + x[i] = (-b + rho * (z - u[i])) / denom + + # z-update: average of x + u + z_old = z + z = sum(x[i] + u[i] for i in range(n)) / n + + # u-update + for i in range(n): + u[i] = u[i] + x[i] - z + + history.append(z) + + # check convergence (primal residual) + r_norm = math.sqrt(sum((x[i] - z) ** 2 for i in range(n))) + s_norm = math.sqrt(n) * abs(rho * (z - z_old)) + if r_norm < tol and s_norm < tol: + break + + return z, history diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..2491dac --- /dev/null +++ b/test.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Installing dev dependencies (build, pytest)..." +pip install -U build pytest >/dev/null + +echo "Installing package in editable mode..." +pip install -e . >/dev/null + +echo "Running pytest..." +pytest -q + +echo "Building distribution..." +python3 -m build + +echo "All done." diff --git a/tests/test_schema.py b/tests/test_schema.py new file mode 100644 index 0000000..e5f9152 --- /dev/null +++ b/tests/test_schema.py @@ -0,0 +1,9 @@ +from idea36_catopt_play_category import contracts + + +def test_export_schemas_contains_models(): + schemas = contracts.export_json_schemas() + expected = ["LocalProblem", "PlanDelta", "AuditLog"] + for name in expected: + assert name in schemas + assert isinstance(schemas[name], dict) diff --git a/tests/test_solver.py b/tests/test_solver.py new file mode 100644 index 0000000..86290f5 --- /dev/null +++ b/tests/test_solver.py @@ -0,0 +1,16 @@ +from idea36_catopt_play_category.solver import admm_consensus + + +def test_admm_consensus_converges_to_average(): + # two agents with objectives 0.5*(x - c)^2 => a=1, b=-c + c1 = 2.0 + c2 = -1.0 + local = [ + {"a": 1.0, "b": -c1}, + {"a": 1.0, "b": -c2}, + ] + + z, history = admm_consensus(local, rho=1.0, max_iter=500, tol=1e-6) + # analytic centralized optimum is average of c1 and c2 + expected = (c1 + c2) / 2.0 + assert abs(z - expected) < 1e-3