diff --git a/src/energiamesh/core.py b/src/energiamesh/core.py index fad2b15..3af4152 100644 --- a/src/energiamesh/core.py +++ b/src/energiamesh/core.py @@ -67,6 +67,40 @@ class GraphOfContracts: return self.contracts.get(contract_id) +@dataclass +class SafetyBudget: + """Budget controls to enforce safety constraints across federation. + + This is a lightweight, pluggable budget model for MVP. It captures a + few conservative defaults and can be extended with domain-specific checks. + """ + enabled: bool = True + max_current_draw_a: float = 0.0 # maximum allowed current draw in amperes + max_voltage_variation_pu: float = 0.0 # per-unit allowable voltage variation + device_limits: Dict[str, float] = field(default_factory=dict) # per-device limits + timestamp: float = field(default_factory=time.time) + + def update(self, key: str, value: Any) -> None: + self.device_limits[key] = value + self.timestamp = time.time() + + +@dataclass +class PrivacyBudget: + """Budget to bound data sharing and protect privacy in federation.""" + enabled: bool = True + allowed_signals: List[str] = field(default_factory=list) + total_budget_units: float = 1.0 # abstract budget units + per_signal_budget: Dict[str, float] = field(default_factory=dict) + timestamp: float = field(default_factory=time.time) + + def use(self, signal: str, amount: float) -> None: + # Simple budgeting semantics: deduct amount from per-signal budget + current = self.per_signal_budget.get(signal, self.total_budget_units) + self.per_signal_budget[signal] = max(0.0, current - amount) + self.timestamp = time.time() + + __all__ = [ "LocalProblem", "SharedVariables", @@ -74,4 +108,6 @@ __all__ = [ "DualVariables", "AuditLog", "GraphOfContracts", + "SafetyBudget", + "PrivacyBudget", ] diff --git a/tests/test_budgets.py b/tests/test_budgets.py new file mode 100644 index 0000000..3de335e --- /dev/null +++ b/tests/test_budgets.py @@ -0,0 +1,20 @@ +from energiamesh.core import SafetyBudget, PrivacyBudget + + +def test_safety_budget_basic(): + sb = SafetyBudget(enabled=True, max_current_draw_a=50.0, max_voltage_variation_pu=0.02) + assert sb.enabled is True + assert sb.max_current_draw_a == 50.0 + assert sb.max_voltage_variation_pu == 0.02 + # Update device limits + sb.update("DER-01", 40.0) + assert sb.device_limits["DER-01"] == 40.0 + + +def test_privacy_budget_basic_and_use(): + pb = PrivacyBudget(enabled=True, allowed_signals=["forecast"], total_budget_units=1.0) + assert pb.enabled is True + assert pb.allowed_signals == ["forecast"] + pb.per_signal_budget["forecast"] = 1.0 + pb.use("forecast", 0.25) + assert pb.per_signal_budget["forecast"] == 0.75