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..cc66b05 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# NovaPlan SWARM Architecture + +This repository contains a minimal, testable MVP for the NovaPlan concept: +- Decentralized, offline-friendly multi-agent planning with privacy-preserving considerations. +- Local problem solving with federated aggregation (ADMM-like) and secure data contracts. +- A lightweight mission ledger anchored when ground links are available. +- Adapters for common hardware (rovers, habitat modules) and simulation-ready interfaces. + +Guiding rules +- Keep changes small and well-scoped. Favor minimal viable features that demonstrate core ideas. +- Tests and packaging must pass before publishing. See test.sh and README for workflow. +- All components are Python-based for this MVP unless the user explicitly requests another language. + +Architecture overview +- planner.py: Local problem and a tiny ADMM-style solver interface. +- contracts.py: Data contracts (PlanDelta, SharedSchedule, ResourceUsage, PrivacyBudget, AuditLog). +- ledger.py: Simple, auditable decision ledger with optional anchoring to external ground links. +- adapters/: Lightweight stubs for rover and habitat module adapters. +- tests/: Unit tests validating the core workflow and contract encoding/decoding. + +Development workflow +- Implement features as independent modules with small, focused tests. +- Run test.sh to verify end-to-end viability, including packaging build check. +- Update README.md with usage notes and API surface. + +Testing commands +- Run tests: ./test.sh +- Build package: python -m build + +Contribution +- Open a PR with a focused feature, include tests, and ensure all tests pass. diff --git a/README.md b/README.md index 8b7268a..2501628 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,26 @@ -# novaplan-decentralized-privacy-preservin +# NovaPlan MVP -A novel open-source software framework enabling offline-first, privacy-preserving coordination and planning across heterogeneous space robotics fleets (rovers, drones, habitat bots) operating in deep-space habitats or spacecraft with intermittent com \ No newline at end of file +A minimal, open-source MVP for decentralized, privacy-preserving multi-agent mission planning in deep-space robotic constellations. + +- Offline-first, privacy-aware coordination across heterogeneous fleets (rovers, drones, habitat bots). +- Local problem solving with a tiny ADMM-like core and federation of agents. +- A lightweight mission ledger that can anchor decisions when ground links are available. +- Lightweight adapters for common hardware and simulation-ready interfaces. +- A clear test and packaging path to verify end-to-end viability. + +This repository focuses on a small, well-scoped subset of the NovaPlan ecosystem to demonstrate core ideas and enable further expansion. + +How to run tests +- Run: `./test.sh` +- This will execute unit tests and verify packaging with `python -m build`. + +Directory layout +- nova_plan/ Core MVP implementation (planner, contracts, ledger, adapters) +- tests/ Unit tests for core workflow and contracts +- adapters/ Stubs for rover and habitat modules +- README.md, AGENTS.md Documentation and governance for the project +- pyproject.toml Build metadata for packaging +- AGENTS.md Architecture and contribution guidelines +- READY_TO_PUBLISH (created after the repo is ready for publishing) + +Note: This is a minimal MVP intended for demonstration and testing; it is not a production-ready system. diff --git a/nova_plan/__init__.py b/nova_plan/__init__.py new file mode 100644 index 0000000..676b634 --- /dev/null +++ b/nova_plan/__init__.py @@ -0,0 +1,3 @@ +"""NovaPlan MVP package init""" + +__all__ = ["planner", "contracts", "ledger", "adapters"] diff --git a/nova_plan/adapters/.gitkeep b/nova_plan/adapters/.gitkeep new file mode 100644 index 0000000..c240a65 --- /dev/null +++ b/nova_plan/adapters/.gitkeep @@ -0,0 +1 @@ +# keep directory for adapters diff --git a/nova_plan/adapters/habitat_adapter.py b/nova_plan/adapters/habitat_adapter.py new file mode 100644 index 0000000..999c10b --- /dev/null +++ b/nova_plan/adapters/habitat_adapter.py @@ -0,0 +1,12 @@ +"""Lightweight habitat module adapter stub for NovaPlan MVP.""" +from __future__ import annotations + +class HabitatAdapter: + def __init__(self, module_id: str): + self.module_id = module_id + + def get_status(self) -> dict: + return {"module_id": self.module_id, "status": "ready"} + + def plan_task(self, task: dict) -> dict: + return {"module_id": self.module_id, "accepted": True, "task": task} diff --git a/nova_plan/adapters/rover_adapter.py b/nova_plan/adapters/rover_adapter.py new file mode 100644 index 0000000..d376781 --- /dev/null +++ b/nova_plan/adapters/rover_adapter.py @@ -0,0 +1,14 @@ +"""Lightweight rover adapter stub for NovaPlan MVP.""" +from __future__ import annotations + +class RoverAdapter: + def __init__(self, rover_id: str): + self.rover_id = rover_id + + def get_status(self) -> dict: + # In a real adapter this would fetch telemetry; here we return a stub. + return {"rover_id": self.rover_id, "status": "idle"} + + def plan_task(self, task: dict) -> dict: + # Accept a plan and return an acknowledgment. + return {"rover_id": self.rover_id, "accepted": True, "task": task} diff --git a/nova_plan/contracts.py b/nova_plan/contracts.py new file mode 100644 index 0000000..854bb5d --- /dev/null +++ b/nova_plan/contracts.py @@ -0,0 +1,49 @@ +"""Simple data contracts used by NovaPlan MVP. + +- PlanDelta: delta between local and global plans. +- SharedSchedule: aggregated schedule signals from agents. +- ResourceUsage: energy, time, or other resource consumptions. +- PrivacyBudget: basic DP-like budget for an agent (simulated). +- AuditLog: lightweight log entries for governance. +""" +from __future__ import annotations +from dataclasses import dataclass, asdict +from typing import Dict, Any, List +import json + +@dataclass +class PlanDelta: + agent_id: str + delta: Dict[str, float] + timestamp: float + + def to_json(self) -> str: + return json.dumps(asdict(self)) + +@dataclass +class SharedSchedule: + schedule: Dict[str, Any] + timestamp: float + +@dataclass +class ResourceUsage: + agent_id: str + resources: Dict[str, float] + timestamp: float + +@dataclass +class PrivacyBudget: + agent_id: str + budget: float + timestamp: float + +@dataclass +class AuditLog: + entry_id: str + message: str + timestamp: float + +def serialize(obj: object) -> str: + if hasattr(obj, "__dict__"): + return json.dumps(obj.__dict__) + return json.dumps(obj) diff --git a/nova_plan/ledger.py b/nova_plan/ledger.py new file mode 100644 index 0000000..704b3f9 --- /dev/null +++ b/nova_plan/ledger.py @@ -0,0 +1,33 @@ +"""A lightweight mission ledger with optional anchoring capability. + +- append-only log of decisions +- optional anchoring to an external ground link (simulated for MVP) +""" +from __future__ import annotations +from datetime import datetime +from typing import List + +class LedgerEntry: + def __init__(self, key: str, value: str, anchor: str | None = None): + self.key = key + self.value = value + self.timestamp = datetime.utcnow().isoformat() + self.anchor = anchor + + def __repr__(self) -> str: + return f"LedgerEntry(key={self.key}, timestamp={self.timestamp}, anchor={self.anchor})" + +class Ledger: + def __init__(self): + self.entries: List[LedgerEntry] = [] + + def log(self, key: str, value: str, anchor: str | None = None) -> LedgerEntry: + e = LedgerEntry(key, value, anchor) + self.entries.append(e) + return e + + def last_anchor(self) -> str | None: + for e in reversed(self.entries): + if e.anchor: + return e.anchor + return None diff --git a/nova_plan/planner.py b/nova_plan/planner.py new file mode 100644 index 0000000..9dad103 --- /dev/null +++ b/nova_plan/planner.py @@ -0,0 +1,41 @@ +"""Minimal local planning core for NovaPlan MVP. + +This module provides a tiny LocalProblem definition and a naive ADMM-like +heartbeat to combine local objectives with a shared variable. The implementation +is intentionally small and deterministic to enable unit tests and demonstrations. +""" +from __future__ import annotations +from typing import Dict, Any + +class LocalProblem: + """A tiny local optimization problem. + + Attributes: + id: Unique identifier for the agent. + objective: A callable that computes a scalar objective given a dict of variables. + variables: Local decision variables for this agent. + constraints: Optional dict describing simple bound constraints. + """ + + def __init__(self, id: str, objective, variables: Dict[str, float], constraints: Dict[str, Any] | None = None): + self.id = id + self.objective = objective + self.variables = variables + self.constraints = constraints or {} + + def evaluate(self, shared_vars: Dict[str, float]) -> float: + """Evaluate local objective using local variables and shared_vars.""" + return float(self.objective(self.variables, shared_vars)) + +def simple_admm_step(local: LocalProblem, shared_vars: Dict[str, float], rho: float = 1.0) -> Dict[str, float]: + """Perform a toy ADMM-like update step and return updated local variables. + + This is intentionally simple: we adjust each local variable toward the corresponding + shared variable with a step proportional to the difference times rho. + """ + new_vars = {} + for k, v in local.variables.items(): + s = shared_vars.get(k, 0.0) + new_vars[k] = v + rho * (s - v) + local.variables = new_vars + return new_vars diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9841d82 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "novaplan-mvp" +version = "0.1.0" +description = "Minimal MVP for decentralized, privacy-preserving multi-agent mission planning in space robotics" +readme = "README.md" +requires-python = ">=3.8" + +[tool.setuptools.packages.find] +where = ["."] diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..b459308 --- /dev/null +++ b/test.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Installing package in editable mode..." +pip install -e . >/dev/null 2>&1 || true + +echo "Running unit tests..." +pytest -q + +echo "Building package..." +python -m build + +echo "All tests passed and package built." diff --git a/tests/test_core.py b/tests/test_core.py new file mode 100644 index 0000000..4e642ff --- /dev/null +++ b/tests/test_core.py @@ -0,0 +1,37 @@ +import time +from nova_plan.planner import LocalProblem, simple_admm_step +from nova_plan.contracts import PlanDelta +from nova_plan.ledger import Ledger + + +def test_local_problem_evaluation_and_admm_step(): + # Simple local problem: minimize (x - s)^2 with x in [0, 10], shared_var s + def objective(local_vars, shared): + x = local_vars.get("x", 0.0) + s = shared.get("x", 0.0) + return (x - s) ** 2 + + lp = LocalProblem(id="agent-1", objective=objective, variables={"x": 5.0}, constraints={"min": 0, "max": 10}) + shared = {"x": 7.0} + + # Evaluate + val = lp.evaluate(shared) + assert isinstance(val, float) + + # Perform a few ADMM-like steps + for _ in range(3): + simple_admm_step(lp, shared, rho=1.0) + assert isinstance(lp.variables["x"], float) + + +def test_plan_delta_serialization(): + pd = PlanDelta(agent_id="agent-1", delta={"x": 1.0}, timestamp=time.time()) + s = pd.to_json() if hasattr(pd, "to_json") else None + assert s is not None + + +def test_ledger_logging_and_anchor(): + ledger = Ledger() + e = ledger.log("decision", "alloc-1", anchor="ground-link-001") + assert e.anchor == "ground-link-001" + assert ledger.last_anchor() == "ground-link-001"