idea174-catopt-swarm/catopt_swarm/models.py

120 lines
3.3 KiB
Python

from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from pydantic import BaseModel, Field, field_validator, model_validator
class LocalProblem(BaseModel):
robot_id: str
target_pos: float
initial_pos: float = 0.0
current_pos: float | None = None
dual_var: float = 0.0
max_step: float = 15.0
energy_budget: float = 100.0
weight: float = 1.0
version: int = 0
metadata: dict[str, Any] = Field(default_factory=dict)
@field_validator("max_step", "energy_budget", "weight")
@classmethod
def _positive(cls, value: float) -> float:
if value <= 0:
raise ValueError("must be positive")
return value
@model_validator(mode="after")
def _default_current_pos(self) -> "LocalProblem":
if self.current_pos is None:
self.current_pos = self.initial_pos
return self
@property
def travel_distance(self) -> float:
current_pos = self.current_pos if self.current_pos is not None else self.initial_pos
return abs(current_pos - self.initial_pos)
def apply_updates(self, updates: dict[str, Any]) -> None:
for key, value in updates.items():
if hasattr(self, key):
setattr(self, key, value)
class SharedVariables(BaseModel):
version: int = 0
global_consensus: float = 0.0
delta_clock: int = 0
class DualVariables(BaseModel):
values: dict[str, float] = Field(default_factory=dict)
class PlanDelta(BaseModel):
author: str
contract_id: str
sequence: int
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
target_robot_id: str | None = None
updates: dict[str, Any] = Field(default_factory=dict)
@field_validator("sequence")
@classmethod
def _non_negative(cls, value: int) -> int:
if value < 0:
raise ValueError("sequence must be non-negative")
return value
class SafetyPolicy(BaseModel):
max_travel_distance: float = 15.0
min_separation: float = 1.0
energy_budget: float = 100.0
bounded_staleness: int = 2
@field_validator("max_travel_distance", "min_separation", "energy_budget")
@classmethod
def _non_negative(cls, value: float) -> float:
if value < 0:
raise ValueError("must be non-negative")
return value
@field_validator("bounded_staleness")
@classmethod
def _bounded_staleness(cls, value: int) -> int:
if value < 0:
raise ValueError("must be non-negative")
return value
def verify(self, start_pos: float, new_pos: float) -> bool:
return abs(new_pos - start_pos) <= self.max_travel_distance
class AuditLogEntry(BaseModel):
iteration: int
shared_version: int
consensus: float
primal_residual: float
dual_residual: float
applied_deltas: list[str] = Field(default_factory=list)
notes: list[str] = Field(default_factory=list)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class ConvergenceCertificate(BaseModel):
converged: bool
iterations: int
epsilon: float
final_primal_residual: float
final_dual_residual: float
class SwarmSolution(BaseModel):
consensus: float
robots: list[LocalProblem]
shared: SharedVariables
audit_log: list[AuditLogEntry]
certificate: ConvergenceCertificate