← 3.1 Agent PAL core demos

[3.3.2] pal_core_02_predict.py (BINGO) 26.0411 (02 road network / prediction)


drones


Bottom line: This is not a toy script. It demonstrates a real class of operational intelligence systems.

This should represent the second core function:
Predict downstream effects of a change in a system.
Clean v1 concept
Use a deterministic network model first.
No AI required initially.
________________________________________
Goal
Given a disruption, predict:
•	reroutes 
•	delays 
•	overloaded nodes 
•	impacted destinations 
________________________________________
Best first demo
Road network
A -> B -> C
A -> D -> C
Normal path:
A-B-C
Disruption:
Block B-C
Prediction:
Traffic reroutes to A-D-C
Delay increases
Node D load increases
________________________________________
Why this is best
•	visual 
•	easy logic 
•	very relevant 
•	deterministic 
•	expandable later 
# pal_core_02_predict.py
#
# PAL Core 02 - Predict
#
# Commands:
#   1) demo        -> load demo network, show baseline, apply demo block, show prediction
#   2) status      -> show current network state and predicted routes
#   3) reset       -> reset to demo network with no blocked edges
#   4) block A B   -> block directed edge A -> B, then show prediction
#   5) unblock A B -> unblock directed edge A -> B, then show prediction
#
# Examples:
#   python pal_core_02_predict.py demo
#   python pal_core_02_predict.py status
#   python pal_core_02_predict.py block B C
#   python pal_core_02_predict.py unblock B C
#   python pal_core_02_predict.py reset
#
# Notes:
# - Deterministic only
# - No AI needed in v1
# - Uses a small hardcoded network
# - Predicts downstream effects of blocked roads/edges

import json
import heapq
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple

# --------------------------------------------------
# 1 FILES / CONSTANTS
# --------------------------------------------------
STATE_FILE = Path("pal_core_predict_state.json")

DEMO_NETWORK = {
    "nodes": ["A", "B", "C", "D", "E"],
    "edges": [
        {"from": "A", "to": "B", "cost": 2},
        {"from": "B", "to": "C", "cost": 2},
        {"from": "A", "to": "D", "cost": 3},
        {"from": "D", "to": "C", "cost": 2},
        {"from": "B", "to": "D", "cost": 1},
        {"from": "D", "to": "E", "cost": 2},
        {"from": "E", "to": "C", "cost": 1},
    ],
    "routes": [
        {"route_id": "ROUTE_001", "from": "A", "to": "C"},
        {"route_id": "ROUTE_002", "from": "B", "to": "C"},
        {"route_id": "ROUTE_003", "from": "A", "to": "E"},
    ],
    "blocked_edges": [],
}

# --------------------------------------------------
# 2 HELPERS
# --------------------------------------------------
def make_edge_key(u: str, v: str) -> str:
    return f"{u}->{v}"

def parse_edge_key(key: str) -> Tuple[str, str]:
    parts = key.split("->")
    if len(parts) != 2:
        raise ValueError(f"Invalid edge key: {key}")
    return parts[0], parts[1]

def build_adjacency(state: Dict[str, Any], blocked_edges: Optional[Set[str]] = None) -> Dict[str, List[Tuple[str, int]]]:
    if blocked_edges is None:
        blocked_edges = set(state.get("blocked_edges", []))

    adj: Dict[str, List[Tuple[str, int]]] = {n: [] for n in state["nodes"]}
    for e in state["edges"]:
        u = e["from"]
        v = e["to"]
        c = int(e["cost"])
        if make_edge_key(u, v) in blocked_edges:
            continue
        adj[u].append((v, c))
    return adj

def dijkstra_path(state: Dict[str, Any], start: str, goal: str, blocked_edges: Optional[Set[str]] = None) -> Optional[Dict[str, Any]]:
    adj = build_adjacency(state, blocked_edges)
    pq: List[Tuple[int, str, List[str]]] = [(0, start, [start])]
    best_cost: Dict[str, int] = {start: 0}

    while pq:
        cost, node, path = heapq.heappop(pq)

        if node == goal:
            return {
                "path": path,
                "cost": cost,
            }

        if cost > best_cost.get(node, 10**18):
            continue

        for nxt, edge_cost in adj.get(node, []):
            new_cost = cost + edge_cost
            if new_cost < best_cost.get(nxt, 10**18):
                best_cost[nxt] = new_cost
                heapq.heappush(pq, (new_cost, nxt, path + [nxt]))

    return None

def compute_route_predictions(state: Dict[str, Any], blocked_edges: Optional[Set[str]] = None) -> List[Dict[str, Any]]:
    preds: List[Dict[str, Any]] = []
    for route in state["routes"]:
        rid = route["route_id"]
        start = route["from"]
        goal = route["to"]

        baseline = dijkstra_path(state, start, goal, blocked_edges=set())
        current = dijkstra_path(state, start, goal, blocked_edges=blocked_edges)

        row: Dict[str, Any] = {
            "route_id": rid,
            "from": start,
            "to": goal,
            "baseline_path": baseline["path"] if baseline else None,
            "baseline_cost": baseline["cost"] if baseline else None,
            "current_path": current["path"] if current else None,
            "current_cost": current["cost"] if current else None,
            "status": "ok",
            "extra_cost": 0,
            "rerouted": False,
        }

        if baseline is None and current is None:
            row["status"] = "no_path_defined"
        elif baseline is not None and current is None:
            row["status"] = "blocked"
        elif baseline is None and current is not None:
            row["status"] = "new_path_found"
        else:
            row["extra_cost"] = int(current["cost"]) - int(baseline["cost"])
            row["rerouted"] = current["path"] != baseline["path"]
            row["status"] = "rerouted" if row["rerouted"] else "ok"

        preds.append(row)

    return preds

def compute_hotspots(predictions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    baseline_counts: Dict[str, int] = {}
    current_counts: Dict[str, int] = {}

    def add_counts(path: Optional[List[str]], bucket: Dict[str, int]) -> None:
        if not path:
            return
        for node in path[1:-1]:
            bucket[node] = bucket.get(node, 0) + 1

    for p in predictions:
        add_counts(p.get("baseline_path"), baseline_counts)
        add_counts(p.get("current_path"), current_counts)

    all_nodes = sorted(set(baseline_counts.keys()) | set(current_counts.keys()))
    hotspots: List[Dict[str, Any]] = []
    for node in all_nodes:
        base_n = baseline_counts.get(node, 0)
        curr_n = current_counts.get(node, 0)
        delta = curr_n - base_n
        if delta > 0:
            hotspots.append({
                "node": node,
                "baseline_load": base_n,
                "current_load": curr_n,
                "delta": delta,
            })

    hotspots.sort(key=lambda x: (-x["delta"], x["node"]))
    return hotspots

def summarize_prediction(state: Dict[str, Any]) -> Dict[str, Any]:
    blocked_edges = set(state.get("blocked_edges", []))
    predictions = compute_route_predictions(state, blocked_edges)
    hotspots = compute_hotspots(predictions)

    impacted_routes = sum(1 for p in predictions if p["status"] in ("rerouted", "blocked"))
    blocked_routes = sum(1 for p in predictions if p["status"] == "blocked")
    rerouted_routes = sum(1 for p in predictions if p["status"] == "rerouted")
    total_extra_cost = sum(int(p.get("extra_cost", 0)) for p in predictions if p["current_path"] is not None)

    return {
        "blocked_edges": sorted(blocked_edges),
        "route_predictions": predictions,
        "impacted_routes": impacted_routes,
        "blocked_routes": blocked_routes,
        "rerouted_routes": rerouted_routes,
        "total_extra_cost": total_extra_cost,
        "hotspots": hotspots,
    }

def save_state(state: Dict[str, Any]) -> None:
    STATE_FILE.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")

def load_state() -> Dict[str, Any]:
    if not STATE_FILE.exists():
        return json.loads(json.dumps(DEMO_NETWORK))
    data = json.loads(STATE_FILE.read_text(encoding="utf-8"))
    return data

def reset_state() -> Dict[str, Any]:
    state = json.loads(json.dumps(DEMO_NETWORK))
    save_state(state)
    return state

def print_usage() -> None:
    print(
        "Usage:\n"
        "  python pal_core_02_predict.py demo\n"
        "  python pal_core_02_predict.py status\n"
        "  python pal_core_02_predict.py reset\n"
        "  python pal_core_02_predict.py block <FROM> <TO>\n"
        "  python pal_core_02_predict.py unblock <FROM> <TO>\n\n"
        "Examples:\n"
        "  python pal_core_02_predict.py demo\n"
        "  python pal_core_02_predict.py status\n"
        "  python pal_core_02_predict.py block B C\n"
        "  python pal_core_02_predict.py unblock B C\n"
        "  python pal_core_02_predict.py reset"
    )

def print_state_and_prediction(state: Dict[str, Any]) -> None:
    print("=== NETWORK STATE ===")
    print(json.dumps({
        "nodes": state["nodes"],
        "edges": state["edges"],
        "routes": state["routes"],
        "blocked_edges": state["blocked_edges"],
    }, indent=2, ensure_ascii=False))

    print("\n=== PREDICTION ===")
    print(json.dumps(summarize_prediction(state), indent=2, ensure_ascii=False))

# --------------------------------------------------
# 3 COMMANDS
# --------------------------------------------------
def cmd_demo() -> None:
    state = reset_state()
    print("DEMO OK")
    print(f"Saved state to: {STATE_FILE.resolve()}")
    print("\n=== BASELINE ===")
    print(json.dumps(summarize_prediction(state), indent=2, ensure_ascii=False))

    edge_key = make_edge_key("B", "C")
    state["blocked_edges"] = [edge_key]
    save_state(state)

    print("\n=== APPLY DEMO BLOCK ===")
    print(f"Blocked edge: {edge_key}")

    print("\n=== PREDICTION AFTER BLOCK ===")
    print(json.dumps(summarize_prediction(state), indent=2, ensure_ascii=False))

def cmd_status() -> None:
    state = load_state()
    print_state_and_prediction(state)

def cmd_reset() -> None:
    state = reset_state()
    print("RESET OK")
    print(f"Saved state to: {STATE_FILE.resolve()}")
    print_state_and_prediction(state)

def cmd_block(u: str, v: str) -> None:
    state = load_state()
    edge_key = make_edge_key(u, v)

    valid_edges = {make_edge_key(e["from"], e["to"]) for e in state["edges"]}
    if edge_key not in valid_edges:
        print("BLOCK FAILED")
        print(f"Edge not found: {edge_key}")
        return

    blocked = set(state.get("blocked_edges", []))
    blocked.add(edge_key)
    state["blocked_edges"] = sorted(blocked)
    save_state(state)

    print("BLOCK OK")
    print(f"Blocked edge: {edge_key}")
    print_state_and_prediction(state)

def cmd_unblock(u: str, v: str) -> None:
    state = load_state()
    edge_key = make_edge_key(u, v)

    blocked = set(state.get("blocked_edges", []))
    if edge_key not in blocked:
        print("UNBLOCK OK")
        print(f"Edge was not blocked: {edge_key}")
        print_state_and_prediction(state)
        return

    blocked.remove(edge_key)
    state["blocked_edges"] = sorted(blocked)
    save_state(state)

    print("UNBLOCK OK")
    print(f"Unblocked edge: {edge_key}")
    print_state_and_prediction(state)

# --------------------------------------------------
# 4 MAIN // cmd_demo, _status, _reset, _block, _unblock
# --------------------------------------------------
def main() -> None:
    if len(sys.argv) < 2:
        print_usage()
        return

    command = sys.argv[1].strip().lower()

    if command == "demo":
        cmd_demo()

    elif command == "status":
        cmd_status()

    elif command == "reset":
        cmd_reset()

    elif command == "block":
        if len(sys.argv) < 4:
            print("Missing FROM and TO for block.\n")
            print_usage()
            return
        cmd_block(sys.argv[2], sys.argv[3])

    elif command == "unblock":
        if len(sys.argv) < 4:
            print("Missing FROM and TO for unblock.\n")
            print_usage()
            return
        cmd_unblock(sys.argv[2], sys.argv[3])

    else:
        print(f"Unknown command: {command}\n")
        print_usage()

if __name__ == "__main__":
    main()
#548 test
1 python pal_core_02_predict.py demo

$ python pal_core_02_predict.py demo
DEMO OK
Saved state to: C:\Users\terry\Downloads\d1_agent\pal_core_predict_state.json

=== BASELINE ===
{
  "blocked_edges": [],
  "route_predictions": [
    {
      "route_id": "ROUTE_001",
      "from": "A",
      "to": "C",
      "baseline_path": [
        "A",
        "B",
        "C"
      ],
      "baseline_cost": 4,
      "current_path": [
        "A",
        "B",
        "C"
      ],
      "current_cost": 4,
      "status": "ok",
      "extra_cost": 0,
      "rerouted": false
    },
    {
      "route_id": "ROUTE_002",
      "from": "B",
      "to": "C",
      "baseline_path": [
        "B",
        "C"
      ],
      "baseline_cost": 2,
      "current_path": [
        "B",
        "C"
      ],
      "current_cost": 2,
      "status": "ok",
      "extra_cost": 0,
      "rerouted": false
    },
    {
      "route_id": "ROUTE_003",
      "from": "A",
      "to": "E",
      "baseline_path": [
        "A",
        "D",
        "E"
      ],
      "baseline_cost": 5,
      "current_path": [
        "A",
        "D",
        "E"
      ],
      "current_cost": 5,
      "status": "ok",
      "extra_cost": 0,
      "rerouted": false
    }
  ],
  "impacted_routes": 0,
  "blocked_routes": 0,
  "rerouted_routes": 0,
  "total_extra_cost": 0,
  "hotspots": []
}

=== APPLY DEMO BLOCK ===
Blocked edge: B->C

=== PREDICTION AFTER BLOCK ===
{
  "blocked_edges": [
    "B->C"
  ],
  "route_predictions": [
    {
      "route_id": "ROUTE_001",
      "from": "A",
      "to": "C",
      "baseline_path": [
        "A",
        "B",
        "C"
      ],
      "baseline_cost": 4,
      "current_path": [
        "A",
        "D",
        "C"
      ],
      "current_cost": 5,
      "status": "rerouted",
      "extra_cost": 1,
      "rerouted": true
    },
    {
      "route_id": "ROUTE_002",
      "from": "B",
      "to": "C",
      "baseline_path": [
        "B",
        "C"
      ],
      "baseline_cost": 2,
      "current_path": [
        "B",
        "D",
        "C"
      ],
      "current_cost": 3,
      "status": "rerouted",
      "extra_cost": 1,
      "rerouted": true
    },
    {
      "route_id": "ROUTE_003",
      "from": "A",
      "to": "E",
      "baseline_path": [
        "A",
        "D",
        "E"
      ],
      "baseline_cost": 5,
      "current_path": [
        "A",
        "D",
        "E"
      ],
      "current_cost": 5,
      "status": "ok",
      "extra_cost": 0,
      "rerouted": false
    }
  ],
  "impacted_routes": 2,
  "blocked_routes": 0,
  "rerouted_routes": 2,
  "total_extra_cost": 2,
  "hotspots": [
    {
      "node": "D",
      "baseline_load": 1,
      "current_load": 3,
      "delta": 2
    }
  ]
}
(venv) 
terry@LAPTOP-HKPDHF7M MINGW64 ~/Downloads/d1_agent (main)

4 python pal_core_02_predict.py block B C

$ python pal_core_02_predict.py block B C
BLOCK OK
Blocked edge: B->C
=== NETWORK STATE ===
{
  "nodes": [
    "A",
    "B",
    "C",
    "D",
    "E"
  ],
  "edges": [
    {
      "from": "A",
      "to": "B",
      "cost": 2
    },
    {
      "from": "B",
      "to": "C",
      "cost": 2
    },
    {
      "from": "A",
      "to": "D",
      "cost": 3
    },
    {
      "from": "D",
      "to": "C",
      "cost": 2
    },
    {
      "from": "B",
      "to": "D",
      "cost": 1
    },
    {
      "from": "D",
      "to": "E",
      "cost": 2
    },
    {
      "from": "E",
      "to": "C",
      "cost": 1
    }
  ],
  "routes": [
    {
      "route_id": "ROUTE_001",
      "from": "A",
      "to": "C"
    },
    {
      "route_id": "ROUTE_002",
      "from": "B",
      "to": "C"
    },
    {
      "route_id": "ROUTE_003",
      "from": "A",
      "to": "E"
    }
  ],
  "blocked_edges": [
    "B->C"
  ]
}

=== PREDICTION ===
{
  "blocked_edges": [
    "B->C"
  ],
  "route_predictions": [
    {
      "route_id": "ROUTE_001",
      "from": "A",
      "to": "C",
      "baseline_path": [
        "A",
        "B",
        "C"
      ],
      "baseline_cost": 4,
      "current_path": [
        "A",
        "D",
        "C"
      ],
      "current_cost": 5,
      "status": "rerouted",
      "extra_cost": 1,
      "rerouted": true
    },
    {
      "route_id": "ROUTE_002",
      "from": "B",
      "to": "C",
      "baseline_path": [
        "B",
        "C"
      ],
      "baseline_cost": 2,
      "current_path": [
        "B",
        "D",
        "C"
      ],
      "current_cost": 3,
      "status": "rerouted",
      "extra_cost": 1,
      "rerouted": true
    },
    {
      "route_id": "ROUTE_003",
      "from": "A",
      "to": "E",
      "baseline_path": [
        "A",
        "D",
        "E"
      ],
      "baseline_cost": 5,
      "current_path": [
        "A",
        "D",
        "E"
      ],
      "current_cost": 5,
      "status": "ok",
      "extra_cost": 0,
      "rerouted": false
    }
  ],
  "impacted_routes": 2,
  "blocked_routes": 0,
  "rerouted_routes": 2,
  "total_extra_cost": 2,
  "hotspots": [
    {
      "node": "D",
      "baseline_load": 1,
      "current_load": 3,
      "delta": 2
    }
  ]
}
(venv) 
terry@LAPTOP-HKPDHF7M MINGW64 ~/Downloads/d1_agent (main)

5 python pal_core_02_predict.py unblock B C

$ python pal_core_02_predict.py unblock B C
UNBLOCK OK
Unblocked edge: B->C
=== NETWORK STATE ===
{
  "nodes": [
    "A",
    "B",
    "C",
    "D",
    "E"
  ],
  "edges": [
    {
      "from": "A",
      "to": "B",
      "cost": 2
    },
    {
      "from": "B",
      "to": "C",
      "cost": 2
    },
    {
      "from": "A",
      "to": "D",
      "cost": 3
    },
    {
      "from": "D",
      "to": "C",
      "cost": 2
    },
    {
      "from": "B",
      "to": "D",
      "cost": 1
    },
    {
      "from": "D",
      "to": "E",
      "cost": 2
    },
    {
      "from": "E",
      "to": "C",
      "cost": 1
    }
  ],
  "routes": [
    {
      "route_id": "ROUTE_001",
      "from": "A",
      "to": "C"
    },
    {
      "route_id": "ROUTE_002",
      "from": "B",
      "to": "C"
    },
    {
      "route_id": "ROUTE_003",
      "from": "A",
      "to": "E"
    }
  ],
  "blocked_edges": []
}

=== PREDICTION ===
{
  "blocked_edges": [],
  "route_predictions": [
    {
      "route_id": "ROUTE_001",
      "from": "A",
      "to": "C",
      "baseline_path": [
        "A",
        "B",
        "C"
      ],
      "baseline_cost": 4,
      "current_path": [
        "A",
        "B",
        "C"
      ],
      "current_cost": 4,
      "status": "ok",
      "extra_cost": 0,
      "rerouted": false
    },
    {
      "route_id": "ROUTE_002",
      "from": "B",
      "to": "C",
      "baseline_path": [
        "B",
        "C"
      ],
      "baseline_cost": 2,
      "current_path": [
        "B",
        "C"
      ],
      "current_cost": 2,
      "status": "ok",
      "extra_cost": 0,
      "rerouted": false
    },
    {
      "route_id": "ROUTE_003",
      "from": "A",
      "to": "E",
      "baseline_path": [
        "A",
        "D",
        "E"
      ],
      "baseline_cost": 5,
      "current_path": [
        "A",
        "D",
        "E"
      ],
      "current_cost": 5,
      "status": "ok",
      "extra_cost": 0,
      "rerouted": false
    }
  ],
  "impacted_routes": 0,
  "blocked_routes": 0,
  "rerouted_routes": 0,
  "total_extra_cost": 0,
  "hotspots": []
}
(venv) 
terry@LAPTOP-HKPDHF7M MINGW64 ~/Downloads/d1_agent (main)
What this demo proves
You now have a second real PAL core capability:
Predict consequences of disruption
Input:
Block B->C
System computes:
•	rerouted paths 
•	increased cost 
•	impacted routes 
•	congestion hotspot at D 
That is genuine decision-support behavior.
________________________________________
Why this demo is strong
It shows that AI systems are not only about text.
This is:
•	graph reasoning 
•	what-if simulation 
•	operational planning 
•	system response modeling 
Very valuable category.
________________________________________
Best output line
This one:
"hotspots": [{"node":"D","delta":2}]
That feels very real-world:
A disruption elsewhere overloaded node D.
That is exactly the kind of downstream effect people miss.
________________________________________
Suggested wiki framing
pal_core_02_predict.py
A deterministic simulation demo that predicts how network disruptions propagate through a system.
Concepts shown
•	dependencies 
•	alternate paths 
•	congestion migration 
•	cost increase 
•	resilience analysis 



26.0618