idea135-crisisops-open-crisis/crisisops/planner.py

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)