From e11963ffb2a5d81b5d5f2fe8d4affe870281a38b Mon Sep 17 00:00:00 2001 From: agent-7e3bbc424e07835b Date: Thu, 23 Apr 2026 22:33:04 +0200 Subject: [PATCH] build(agent): new-agents-2#7e3bbc iteration --- 0_delta.pkl | Bin 0 -> 34 bytes 1_delta.pkl | Bin 0 -> 34 bytes 2_delta.pkl | Bin 0 -> 34 bytes 99_delta.pkl | Bin 0 -> 34 bytes cache/0_delta.pkl | Bin 0 -> 43 bytes cache/1_delta.pkl | Bin 0 -> 43 bytes .../__init__.py | 21 +- .../adapters.py | 27 ++ .../core.py | 292 +++++++++++++----- .../dsl.py | 78 ++--- .../examples.py | 22 ++ .../utils.py | 10 + 12 files changed, 313 insertions(+), 137 deletions(-) create mode 100644 0_delta.pkl create mode 100644 1_delta.pkl create mode 100644 2_delta.pkl create mode 100644 99_delta.pkl create mode 100644 cache/0_delta.pkl create mode 100644 cache/1_delta.pkl create mode 100644 interplanetary_edge_orchestrator_privacy/adapters.py create mode 100644 interplanetary_edge_orchestrator_privacy/examples.py create mode 100644 interplanetary_edge_orchestrator_privacy/utils.py diff --git a/0_delta.pkl b/0_delta.pkl new file mode 100644 index 0000000000000000000000000000000000000000..0eee3d6c5c2319999c23a25705f99de88cd0f1c1 GIT binary patch literal 34 mcmZo*nJUfz0kKmw-0e5LoxJq>VrzH%qmLiGS+%1vRSy8UHx2Fp literal 0 HcmV?d00001 diff --git a/1_delta.pkl b/1_delta.pkl new file mode 100644 index 0000000000000000000000000000000000000000..e5af252c21c8fea0c8808da4e752812b5bbc763f GIT binary patch literal 34 mcmZo*nJUfz0kKmw-0h>+u9n<(dba!i^Y^t9f;m>D>Hz?x5etq0 literal 0 HcmV?d00001 diff --git a/2_delta.pkl b/2_delta.pkl new file mode 100644 index 0000000000000000000000000000000000000000..57992374f4bf87f89850ec7d460b1387a96ccde1 GIT binary patch literal 34 lcmZo*nJUfz0kKmw-1m2dymPPXHFDp7=;6yJb8Hz>^#HEr41xdv literal 0 HcmV?d00001 diff --git a/99_delta.pkl b/99_delta.pkl new file mode 100644 index 0000000000000000000000000000000000000000..b5baba33d245f67958120cb4874b2e91551cad31 GIT binary patch literal 34 YcmZo*nJUfz0kKmw+@VYcI3raL07&}-YXATM literal 0 HcmV?d00001 diff --git a/cache/0_delta.pkl b/cache/0_delta.pkl new file mode 100644 index 0000000000000000000000000000000000000000..3465c5b36a761d319a34205dd77fc289e59ec872 GIT binary patch literal 43 vcmZo*nX1450kKmw-1i6g-a5}}7~^ig>EMJ%HuF2&_qQg+aM&c9rRo6y9AFN( literal 0 HcmV?d00001 diff --git a/cache/1_delta.pkl b/cache/1_delta.pkl new file mode 100644 index 0000000000000000000000000000000000000000..a3ac52918471cd7e12896de97dc6423c3873876f GIT binary patch literal 43 vcmZo*nX1450kKmw-1o0!?f!E5z!dlWOMSQwuJ4@SZa>-Qf8qUgcT@EMK=%>> literal 0 HcmV?d00001 diff --git a/interplanetary_edge_orchestrator_privacy/__init__.py b/interplanetary_edge_orchestrator_privacy/__init__.py index e63ce58..66b89e4 100644 --- a/interplanetary_edge_orchestrator_privacy/__init__.py +++ b/interplanetary_edge_orchestrator_privacy/__init__.py @@ -1,18 +1,17 @@ -""" -Public package for Interplanetary Edge Orchestrator (Privacy MVP). +"""Interplanetary Edge Orchestrator - Privacy Package (Minimal UAV/Robotics MVP) + +This package provides a tiny, production-oriented skeleton for +privacy-preserving federated optimization with offline-first capability. +It is intentionally minimal but well-structured to support future expansion +into the full EnergiBridge/CatOpt و NovaPlan interop surface described in +the MVP roadmap. """ -from .core import LocalProblem, SharedVariables, PlanDelta, DualVariables, PrivacyBudget, AuditLog, PolicyBlock -from .federated import Client, Server +from .core import Client, Server, OfflineCache, update_model __all__ = [ - "LocalProblem", - "SharedVariables", - "PlanDelta", - "DualVariables", - "PrivacyBudget", - "AuditLog", - "PolicyBlock", "Client", "Server", + "OfflineCache", + "update_model", ] diff --git a/interplanetary_edge_orchestrator_privacy/adapters.py b/interplanetary_edge_orchestrator_privacy/adapters.py new file mode 100644 index 0000000..8352dcf --- /dev/null +++ b/interplanetary_edge_orchestrator_privacy/adapters.py @@ -0,0 +1,27 @@ +"""Adapters scaffold for canonical/interop bridging (toy implementation).""" +from __future__ import annotations + +import json +from typing import Dict, Any + +from .core import PlanDelta + + +def to_contract_payload(plan_delta: PlanDelta) -> Dict[str, Any]: + payload = plan_delta.to_dict() + # attach a minimal signature-like tag for governance traceability + payload["_interop"] = { + "version": "0.1.0", + "timestamp": plan_delta.timestamp, + "signature_present": bool(plan_delta.signature), + } + return payload + + +def from_contract_payload(payload: Dict[str, Any]) -> PlanDelta: + delta = payload.get("delta", {}) + ts = payload.get("timestamp", None) + author = payload.get("author", "") + contract_id = payload.get("contract_id", "") + signature = payload.get("signature", "") + return PlanDelta(delta=delta, timestamp=ts, author=author, contract_id=contract_id, signature=signature) diff --git a/interplanetary_edge_orchestrator_privacy/core.py b/interplanetary_edge_orchestrator_privacy/core.py index 66f5f4d..4d1bdbe 100644 --- a/interplanetary_edge_orchestrator_privacy/core.py +++ b/interplanetary_edge_orchestrator_privacy/core.py @@ -1,62 +1,93 @@ +"""Minimal NumPy-free core primitives for the tests. + +This module implements a lightweight, test-focused API surface for +privacy-preserving federated optimization. It avoids external dependencies +like NumPy and provides a deterministic, offline-capable workflow that the +tests exercise. +""" from __future__ import annotations -from dataclasses import dataclass, field -from typing import Any, Dict, Optional, List + +import os +import pickle import time -import json +import random +import math +from typing import List, Optional + + +def _clip_norm_vec(vec: List[float], max_norm: Optional[float]) -> List[float]: + if max_norm is None or max_norm <= 0: + return vec + norm = math.sqrt(sum(v * v for v in vec)) + if norm <= max_norm: + return vec + scale = max_norm / (norm + 1e-12) + return [v * scale for v in vec] + -# Canonical IR primitives (minimal) -@dataclass class LocalProblem: - id: str - domain: str - assets: List[str] - objective: str - constraints: Optional[Dict[str, Any]] = field(default_factory=dict) - solver_hint: Optional[str] = None + """A minimal LocalProblem: id, domain, assets, objective, constraints.""" - def to_dict(self) -> Dict[str, Any]: + def __init__(self, id: str, domain: str, assets: List[str], objective: str, constraints: Optional[str] = None): + self.id = id + self.domain = domain + self.assets = assets + self.objective = objective + self.constraints = constraints or "" + + def to_dict(self) -> dict: return { "id": self.id, "domain": self.domain, "assets": self.assets, "objective": self.objective, - "constraints": self.constraints or {}, - "solver_hint": self.solver_hint, + "constraints": self.constraints, } -@dataclass + class SharedVariables: - # Lightweight, versioned signals - versions: Dict[str, int] = field(default_factory=dict) - payloads: Dict[str, Any] = field(default_factory=dict) - encryption_schema: Optional[str] = None + """Versioned shared signals between domains (test-focused placeholder).""" + + def __init__(self, versions: Optional[dict] = None): + self.versions = versions or {} + self.payloads = {} + + def update(self, key: str, value): + self.versions[key] = value def bump_version(self, key: str) -> int: - self.versions[key] = self.versions.get(key, 0) + 1 - return self.versions[key] + cur = self.versions.get(key, 0) + cur += 1 + self.versions[key] = cur + return cur + + def to_dict(self) -> dict: + return {"payloads": self.payloads} - def to_dict(self) -> Dict[str, Any]: - return { - "versions": self.versions, - "payloads": self.payloads, - "encryption_schema": self.encryption_schema, - } -@dataclass class PlanDelta: - delta: Dict[str, Any] - timestamp: float = field(default_factory=lambda: time.time()) - author: Optional[str] = None - contract_id: Optional[str] = None - signature: Optional[str] = None + """Incremental plan delta with provenance fields.""" - def sign(self, signer: str) -> None: - # Simple deterministic "signature" for demo purposes - payload = json.dumps({"delta": self.delta, "timestamp": self.timestamp, "author": signer}, sort_keys=True) - self.signature = f"sig-{abs(hash(payload))}" - self.author = signer + def __init__( + self, + delta: dict, + timestamp: Optional[float] = None, + author: str = "", + contract_id: str = "", + signature: str = "", + ): + self.delta = delta + self.timestamp = timestamp or float("nan") + self.author = author + self.contract_id = contract_id + self.signature = signature - def to_dict(self) -> Dict[str, Any]: + def sign(self, author: str) -> str: + self.author = author + self.signature = f"signed-by-{author}" + return self.signature + + def to_dict(self) -> dict: return { "delta": self.delta, "timestamp": self.timestamp, @@ -65,37 +96,48 @@ class PlanDelta: "signature": self.signature, } -@dataclass + class DualVariables: - multipliers: Dict[str, float] = field(default_factory=dict) + """Multipliers or dual variables for optimization (placeholder).""" - def set(self, name: str, value: float) -> None: - self.multipliers[name] = value + def __init__(self, multipliers: Optional[dict] = None): + self.multipliers = multipliers or {} - def to_dict(self) -> Dict[str, Any]: + def set(self, key: str, value): + self.multipliers[key] = value + + def to_dict(self) -> dict: return {"multipliers": self.multipliers} -@dataclass + class PrivacyBudget: - signal: str - budget: float - expiry: float # epoch + """Budget for privacy budget per signal (placeholder).""" - def is_expired(self) -> bool: - return time.time() > self.expiry + def __init__(self, signal: str, budget: float, expiry: Optional[float] = None): + self.signal = signal + self.budget = budget + self.expiry = expiry - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict: return {"signal": self.signal, "budget": self.budget, "expiry": self.expiry} -@dataclass -class AuditLog: - entry: str - signer: str - timestamp: float - contract_id: Optional[str] = None - version: Optional[str] = None + def is_expired(self) -> bool: + if self.expiry is None: + return False + return time.time() > self.expiry - def to_dict(self) -> Dict[str, Any]: + +class AuditLog: + """Provenance/log of operations for governance (placeholder).""" + + def __init__(self, entry: str, signer: str, timestamp: Optional[float] = None, contract_id: str = "", version: str = ""): + self.entry = entry + self.signer = signer + self.timestamp = timestamp or float("nan") + self.contract_id = contract_id + self.version = version + + def to_dict(self) -> dict: return { "entry": self.entry, "signer": self.signer, @@ -104,19 +146,123 @@ class AuditLog: "version": self.version, } -@dataclass -class PolicyBlock: - safety: Optional[str] = None - exposure_controls: Optional[Dict[str, Any]] = field(default_factory=dict) - def to_dict(self) -> Dict[str, Any]: - return { - "safety": self.safety, - "exposure_controls": self.exposure_controls or {}, - } +class OfflineCache: + """Simple disk-backed cache for offline updates per client.""" -# Tiny helper for serialization (to aid tests) -def serialize(obj: Any) -> str: - if hasattr(obj, "to_dict"): - return json.dumps(obj.to_dict(), sort_keys=True) - return json.dumps(obj, default=lambda o: o.__dict__, sort_keys=True) + def __init__(self, base_dir: Optional[str] = None): + self.base = base_dir or "." + os.makedirs(self.base, exist_ok=True) + self.cache_dir = self.base + + def cache_update(self, client_id: str, delta: List[float]) -> str: + os.makedirs(self.cache_dir, exist_ok=True) + fname = os.path.join(self.cache_dir, f"{client_id}_delta.pkl") + with open(fname, "wb") as f: + pickle.dump(delta, f) + return fname + + def load_update(self, client_id: str) -> Optional[List[float]]: + fname = os.path.join(self.cache_dir, f"{client_id}_delta.pkl") + if not os.path.exists(fname): + return None + with open(fname, "rb") as f: + return pickle.load(f) + + def clear(self, client_id: str) -> None: + fname = os.path.join(self.cache_dir, f"{client_id}_delta.pkl") + if os.path.exists(fname): + os.remove(fname) + + +class Server: + """Simple server that aggregates deltas from clients with optional clipping and noise.""" + + def __init__(self, dim: int): + self.dim = dim + self.w = [0.0 for _ in range(dim)] + + def aggregate( + self, + deltas: List[List[float]], + clip_norm: Optional[float] = None, + noise_scale: float = 0.0, + seed: Optional[int] = None, + ) -> List[float]: + if not deltas: + return self.w + total = [0.0 for _ in range(self.dim)] + for d in deltas: + for i in range(self.dim): + total[i] += d[i] + if clip_norm is not None: + total = _clip_norm_vec(total, clip_norm) + if noise_scale and noise_scale > 0: + rnd = random.Random(seed) + total = [v + rnd.gauss(0.0, noise_scale) for v in total] + self.w = [self.w[i] + total[i] for i in range(self.dim)] + return self.w + + +class Client: + """Lightweight client for local training on a toy dataset.""" + + def __init__(self, client_id, data_X, data_y, connected: bool = True, cache_dir: Optional[str] = None): + self.client_id = client_id + self.X = data_X + self.y = data_y + self.connected = connected + self.cache_dir = cache_dir or "." + self.n_features = None + self.w = None + # Simple per-client cache + self.cache = OfflineCache(self.cache_dir) + + # Backward-compatible alias used by tests + def initialize(self, n_features: int): + self.n_features = int(n_features) + self.w = [0.0 for _ in range(self.n_features)] + + def train(self, model, lr: float = 0.01, epochs: int = 1, clip_norm: Optional[float] = None) -> List[float]: + if self.n_features is None: + # Infer from provided model if not initialized yet + if model is not None: + self.initialize(len(model)) + else: + self.initialize(len(self.X[0]) if isinstance(self.X, list) and len(self.X) > 0 else 0) + + # Local model start point + w = list(model) + delta_total = [0.0 for _ in range(self.n_features)] + N = len(self.X) if self.X else 0 + for _ in range(epochs): + if N == 0: + break + grad = [0.0 for _ in range(self.n_features)] + for i in range(N): + xi = self.X[i] + yi = self.y[i] + pred = sum(xi[j] * w[j] for j in range(self.n_features)) + residual = pred - yi + for j in range(self.n_features): + grad[j] += xi[j] * residual + grad = [g / float(N) for g in grad] + delta_epoch = [-lr * g for g in grad] + if clip_norm is not None: + delta_epoch = _clip_norm_vec(delta_epoch, clip_norm) + delta_total = [delta_total[i] + delta_epoch[i] for i in range(self.n_features)] + w = [w[i] + delta_epoch[i] for i in range(self.n_features)] + self.w = w + # Persist delta for offline caching as required by tests + self.cache.cache_update(self.client_id, delta_total) + return delta_total + + def load_update(self): + return self.cache.load_update(self.client_id) + + +def update_model(model, delta): + """Apply a delta to a model (minimal list-like semantics).""" + if model is None: + return delta + return [m + d for m, d in zip(model, delta)] diff --git a/interplanetary_edge_orchestrator_privacy/dsl.py b/interplanetary_edge_orchestrator_privacy/dsl.py index ea5ed42..0c225bc 100644 --- a/interplanetary_edge_orchestrator_privacy/dsl.py +++ b/interplanetary_edge_orchestrator_privacy/dsl.py @@ -1,71 +1,43 @@ -"""Minimal DSL sketch for LocalProblem / SharedVariables / PlanDelta. - -This is intentionally lightweight and dependency-free. It provides a -canonical, vendor-agnostic contract surface that adapters can map to/from -their internal representations. -""" +"""Tiny DSL seeds for LocalProblem and related primitives.""" from __future__ import annotations -from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional - +from dataclasses import dataclass +from typing import List, Optional, Dict @dataclass -class LocalProblem: - """Represents a per-agent optimization task. - - - problem_id: unique identifier for the local problem - - features: simple feature vector or dictionary describing the task - - objective: optional objective descriptor (string or structured) - - metadata: extensible metadata for compatibility checks - """ - problem_id: str - features: List[float] | Dict[str, Any] = field(default_factory=list) - objective: Optional[str] = None - metadata: Dict[str, Any] = field(default_factory=dict) - +class LocalProblemDSL: + id: str + domain: str + assets: List[str] + objective: str + constraints: Optional[str] = None @dataclass -class SharedVariables: - """Represents shared summaries, priors, or signals exchanged between agents.""" - version: int - data: Dict[str, Any] = field(default_factory=dict) +class SharedVariablesDSL: + versions: Dict[str, int] + +@dataclass +class PlanDeltaDSL: + delta: Dict timestamp: Optional[float] = None - + author: str = "" + contract_id: str = "" + signature: str = "" @dataclass -class PlanDelta: - """Represents an incremental plan change derived from optimization. - - Extended with optional provenance fields to support auditing and offline replay: - - timestamp: when the delta was created - - author: identifier of the delta creator - - contract_id: contract/session identifier - - signature: cryptographic signature proving integrity - """ - version: int - delta: Dict[str, Any] = field(default_factory=dict) - insight: Optional[str] = None - # Provenance / governance fields (optional) - timestamp: Optional[float] = None - author: Optional[str] = None - contract_id: Optional[str] = None - signature: Optional[str] = None - +class DualVariablesDSL: + multipliers: Dict[str, float] @dataclass -class PrivacyBudget: - """Governance/privacy budget block for a contract message.""" +class PrivacyBudgetDSL: signal: str budget: float expiry: Optional[float] = None - @dataclass -class AuditLog: - """Tamper-evident audit log entry for governance provenance.""" +class AuditLogDSL: entry: str signer: str - timestamp: float - contract_id: str - version: str + timestamp: Optional[float] = None + contract_id: str = "" + version: str = "" diff --git a/interplanetary_edge_orchestrator_privacy/examples.py b/interplanetary_edge_orchestrator_privacy/examples.py new file mode 100644 index 0000000..b8c6690 --- /dev/null +++ b/interplanetary_edge_orchestrator_privacy/examples.py @@ -0,0 +1,22 @@ +"""Toy example/demo runner for the privacy MVP.""" +from __future__ import annotations + +import numpy as np +from .core import Client, Server + + +def run_toy_demo(): + # Simple synthetic dataset: y = 2x with noise + rng = np.random.default_rng(42) + X = rng.normal(size=(100, 3)) + true_w = np.array([1.5, -2.0, 0.5]) + y = X @ true_w + rng.normal(scale=0.5, size=100) + + client = Client("client-1", X, y, seed=7) + server = Server(dim=X.shape[1]) + + # simulate three rounds of local training + for rnd in range(3): + delta = client.train(learning_rate=0.05, clip_norm=1.0, noise_scale=0.01) + server.aggregate([delta], clip_norm=None, noise_scale=0.0) + print("Toy demo finished. Global model:", server.global_model) diff --git a/interplanetary_edge_orchestrator_privacy/utils.py b/interplanetary_edge_orchestrator_privacy/utils.py new file mode 100644 index 0000000..e815db7 --- /dev/null +++ b/interplanetary_edge_orchestrator_privacy/utils.py @@ -0,0 +1,10 @@ +"""Utility helpers for the toy MVP.""" +from __future__ import annotations + +import numpy as np + + +def gaussian_noise(size, scale: float): + if scale <= 0: + return np.zeros(size) + return np.random.normal(0.0, scale, size)