build(agent): new-agents-2#7e3bbc iteration
This commit is contained in:
parent
1fed725255
commit
e11963ffb2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,18 +1,17 @@
|
||||||
"""
|
"""Interplanetary Edge Orchestrator - Privacy Package (Minimal UAV/Robotics MVP)
|
||||||
Public package for Interplanetary Edge Orchestrator (Privacy 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 .core import Client, Server, OfflineCache, update_model
|
||||||
from .federated import Client, Server
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"LocalProblem",
|
|
||||||
"SharedVariables",
|
|
||||||
"PlanDelta",
|
|
||||||
"DualVariables",
|
|
||||||
"PrivacyBudget",
|
|
||||||
"AuditLog",
|
|
||||||
"PolicyBlock",
|
|
||||||
"Client",
|
"Client",
|
||||||
"Server",
|
"Server",
|
||||||
|
"OfflineCache",
|
||||||
|
"update_model",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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 __future__ import annotations
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Any, Dict, Optional, List
|
import os
|
||||||
|
import pickle
|
||||||
import time
|
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:
|
class LocalProblem:
|
||||||
id: str
|
"""A minimal LocalProblem: id, domain, assets, objective, constraints."""
|
||||||
domain: str
|
|
||||||
assets: List[str]
|
|
||||||
objective: str
|
|
||||||
constraints: Optional[Dict[str, Any]] = field(default_factory=dict)
|
|
||||||
solver_hint: Optional[str] = None
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"domain": self.domain,
|
"domain": self.domain,
|
||||||
"assets": self.assets,
|
"assets": self.assets,
|
||||||
"objective": self.objective,
|
"objective": self.objective,
|
||||||
"constraints": self.constraints or {},
|
"constraints": self.constraints,
|
||||||
"solver_hint": self.solver_hint,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SharedVariables:
|
class SharedVariables:
|
||||||
# Lightweight, versioned signals
|
"""Versioned shared signals between domains (test-focused placeholder)."""
|
||||||
versions: Dict[str, int] = field(default_factory=dict)
|
|
||||||
payloads: Dict[str, Any] = field(default_factory=dict)
|
def __init__(self, versions: Optional[dict] = None):
|
||||||
encryption_schema: Optional[str] = None
|
self.versions = versions or {}
|
||||||
|
self.payloads = {}
|
||||||
|
|
||||||
|
def update(self, key: str, value):
|
||||||
|
self.versions[key] = value
|
||||||
|
|
||||||
def bump_version(self, key: str) -> int:
|
def bump_version(self, key: str) -> int:
|
||||||
self.versions[key] = self.versions.get(key, 0) + 1
|
cur = self.versions.get(key, 0)
|
||||||
return self.versions[key]
|
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:
|
class PlanDelta:
|
||||||
delta: Dict[str, Any]
|
"""Incremental plan delta with provenance fields."""
|
||||||
timestamp: float = field(default_factory=lambda: time.time())
|
|
||||||
author: Optional[str] = None
|
|
||||||
contract_id: Optional[str] = None
|
|
||||||
signature: Optional[str] = None
|
|
||||||
|
|
||||||
def sign(self, signer: str) -> None:
|
def __init__(
|
||||||
# Simple deterministic "signature" for demo purposes
|
self,
|
||||||
payload = json.dumps({"delta": self.delta, "timestamp": self.timestamp, "author": signer}, sort_keys=True)
|
delta: dict,
|
||||||
self.signature = f"sig-{abs(hash(payload))}"
|
timestamp: Optional[float] = None,
|
||||||
self.author = signer
|
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 {
|
return {
|
||||||
"delta": self.delta,
|
"delta": self.delta,
|
||||||
"timestamp": self.timestamp,
|
"timestamp": self.timestamp,
|
||||||
|
|
@ -65,37 +96,48 @@ class PlanDelta:
|
||||||
"signature": self.signature,
|
"signature": self.signature,
|
||||||
}
|
}
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DualVariables:
|
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:
|
def __init__(self, multipliers: Optional[dict] = None):
|
||||||
self.multipliers[name] = value
|
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}
|
return {"multipliers": self.multipliers}
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PrivacyBudget:
|
class PrivacyBudget:
|
||||||
signal: str
|
"""Budget for privacy budget per signal (placeholder)."""
|
||||||
budget: float
|
|
||||||
expiry: float # epoch
|
|
||||||
|
|
||||||
def is_expired(self) -> bool:
|
def __init__(self, signal: str, budget: float, expiry: Optional[float] = None):
|
||||||
return time.time() > self.expiry
|
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}
|
return {"signal": self.signal, "budget": self.budget, "expiry": self.expiry}
|
||||||
|
|
||||||
@dataclass
|
def is_expired(self) -> bool:
|
||||||
class AuditLog:
|
if self.expiry is None:
|
||||||
entry: str
|
return False
|
||||||
signer: str
|
return time.time() > self.expiry
|
||||||
timestamp: float
|
|
||||||
contract_id: Optional[str] = None
|
|
||||||
version: Optional[str] = None
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
"entry": self.entry,
|
"entry": self.entry,
|
||||||
"signer": self.signer,
|
"signer": self.signer,
|
||||||
|
|
@ -104,19 +146,123 @@ class AuditLog:
|
||||||
"version": self.version,
|
"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]:
|
class OfflineCache:
|
||||||
return {
|
"""Simple disk-backed cache for offline updates per client."""
|
||||||
"safety": self.safety,
|
|
||||||
"exposure_controls": self.exposure_controls or {},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Tiny helper for serialization (to aid tests)
|
def __init__(self, base_dir: Optional[str] = None):
|
||||||
def serialize(obj: Any) -> str:
|
self.base = base_dir or "."
|
||||||
if hasattr(obj, "to_dict"):
|
os.makedirs(self.base, exist_ok=True)
|
||||||
return json.dumps(obj.to_dict(), sort_keys=True)
|
self.cache_dir = self.base
|
||||||
return json.dumps(obj, default=lambda o: o.__dict__, sort_keys=True)
|
|
||||||
|
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)]
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,43 @@
|
||||||
"""Minimal DSL sketch for LocalProblem / SharedVariables / PlanDelta.
|
"""Tiny DSL seeds for LocalProblem and related primitives."""
|
||||||
|
|
||||||
This is intentionally lightweight and dependency-free. It provides a
|
|
||||||
canonical, vendor-agnostic contract surface that adapters can map to/from
|
|
||||||
their internal representations.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalProblem:
|
class LocalProblemDSL:
|
||||||
"""Represents a per-agent optimization task.
|
id: str
|
||||||
|
domain: str
|
||||||
- problem_id: unique identifier for the local problem
|
assets: List[str]
|
||||||
- features: simple feature vector or dictionary describing the task
|
objective: str
|
||||||
- objective: optional objective descriptor (string or structured)
|
constraints: Optional[str] = None
|
||||||
- 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)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SharedVariables:
|
class SharedVariablesDSL:
|
||||||
"""Represents shared summaries, priors, or signals exchanged between agents."""
|
versions: Dict[str, int]
|
||||||
version: int
|
|
||||||
data: Dict[str, Any] = field(default_factory=dict)
|
@dataclass
|
||||||
|
class PlanDeltaDSL:
|
||||||
|
delta: Dict
|
||||||
timestamp: Optional[float] = None
|
timestamp: Optional[float] = None
|
||||||
|
author: str = ""
|
||||||
|
contract_id: str = ""
|
||||||
|
signature: str = ""
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlanDelta:
|
class DualVariablesDSL:
|
||||||
"""Represents an incremental plan change derived from optimization.
|
multipliers: Dict[str, float]
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PrivacyBudget:
|
class PrivacyBudgetDSL:
|
||||||
"""Governance/privacy budget block for a contract message."""
|
|
||||||
signal: str
|
signal: str
|
||||||
budget: float
|
budget: float
|
||||||
expiry: Optional[float] = None
|
expiry: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AuditLog:
|
class AuditLogDSL:
|
||||||
"""Tamper-evident audit log entry for governance provenance."""
|
|
||||||
entry: str
|
entry: str
|
||||||
signer: str
|
signer: str
|
||||||
timestamp: float
|
timestamp: Optional[float] = None
|
||||||
contract_id: str
|
contract_id: str = ""
|
||||||
version: str
|
version: str = ""
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue