90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
"""A lightweight ADMM-like planner for CrisisOps MVP.
|
|
|
|
This is intentionally simple: given a set of available objects (resources)
|
|
and a set of requests (needs), it performs a basic allocation to maximize
|
|
reliability of critical deliveries while minimizing idle trips in a toy setting.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import Any, Dict, List
|
|
|
|
from .core import GraphOfContracts, PlanDelta
|
|
|
|
|
|
class ADMMPlanner:
|
|
def __init__(self):
|
|
# In a real system this would persist state; for MVP we keep in-memory.
|
|
self._state: Dict[str, Dict[str, int]] = {}
|
|
|
|
def reset(self) -> None:
|
|
self._state.clear()
|
|
|
|
def optimize(self, gof: GraphOfContracts, max_iter: int = 5) -> PlanDelta:
|
|
"""Run a toy optimization pass over the GoC to produce a PlanDelta.
|
|
|
|
We allocate available objects to matching morphisms (requests) by a
|
|
deterministic rule: dispatch-style morphisms create actions when the
|
|
source and target exist, and the action payload includes a utility score
|
|
that favors balanced capacity and higher-priority signals.
|
|
"""
|
|
actions: List[Dict[str, object]] = []
|
|
now = time.time()
|
|
|
|
for mid in sorted(gof.morphisms):
|
|
morph = gof.morphisms[mid]
|
|
src = gof.objects.get(morph.source_id)
|
|
dst = gof.objects.get(morph.target_id)
|
|
if not src or not dst:
|
|
continue
|
|
|
|
if morph.type not in {"dispatch", "transfer"} and src.type != dst.type:
|
|
continue
|
|
|
|
action = {
|
|
"morphism_id": mid,
|
|
"action": "dispatch",
|
|
"resource_id": src.id,
|
|
"destination_id": dst.id,
|
|
"timestamp": now,
|
|
"details": {
|
|
"src_type": src.type,
|
|
"dst_type": dst.type,
|
|
"priority": morph.signals.get("priority", "normal"),
|
|
"utility_score": self._utility_score(src.attributes, dst.attributes, morph.signals),
|
|
},
|
|
}
|
|
actions.append(action)
|
|
|
|
actions.sort(key=lambda action: (action["morphism_id"], action["resource_id"], action["destination_id"]))
|
|
|
|
# Record plan delta
|
|
plan = PlanDelta(timestamp=now, actions=actions)
|
|
gof.plan_delta = plan
|
|
return plan
|
|
|
|
@staticmethod
|
|
def _numeric_attribute(attributes: Dict[str, Any], keys: List[str], default: float = 0.0) -> float:
|
|
for key in keys:
|
|
value = attributes.get(key)
|
|
if value is None:
|
|
continue
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
return default
|
|
|
|
def _utility_score(
|
|
self,
|
|
src_attributes: Dict[str, object],
|
|
dst_attributes: Dict[str, object],
|
|
signals: Dict[str, object],
|
|
) -> float:
|
|
source_capacity = self._numeric_attribute(src_attributes, ["capacity", "stock", "qty", "available"], default=1.0)
|
|
target_need = self._numeric_attribute(dst_attributes, ["demand", "capacity", "need", "qty"], default=1.0)
|
|
balance = min(source_capacity, target_need) / max(source_capacity, target_need, 1.0)
|
|
priority = str(signals.get("priority", "normal"))
|
|
priority_boost = {"low": 0.9, "normal": 1.0, "high": 1.1, "critical": 1.25}.get(priority, 1.0)
|
|
return round(min(1.0, balance * priority_boost), 3)
|