File size: 5,212 Bytes
cdfd467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import math
import random
from collections import defaultdict

# --- Configuration (Defaults) ---
AREA_SIZE = 1000.0
N_CLUSTERS = 5
T_MAX = 30
ALPHA = 0.5; BETA = 0.3; KAPPA = 0.1; ZETA = 0.1

def dist(a, b): return math.hypot(a[0]-b[0], a[1]-b[1])

class Node:
    def __init__(self, idx, pos, cluster):
        self.idx = idx; self.pos = pos; self.cluster = int(cluster)
        self.batt = 100.0; self.dead = False; self.S = 0.0; self.fair = 0.0
        self.is_head = False; self.head_since = 0
        self.dead_since = None

    def get_color(self):
        if self.dead: return '#ff0000' # Red
        if self.batt > 50: return '#00ff00' # Green
        if self.batt > 20: return '#ffff00' # Yellow
        return '#ff9900' # Orange

    def consume(self, amount, sim_time):
        if self.dead: return
        self.batt -= amount
        if self.batt <= 0: 
            self.batt = 0; self.dead = True; self.is_head = False
            self.dead_since = sim_time

def calculate_utility(node, gateway):
    term_S = min(node.S, 1.0); term_E = node.batt / 100.0
    term_fair = min(node.fair, 1.0)
    d_gate = dist(node.pos, gateway)
    term_lq = 1.0 - (d_gate / (AREA_SIZE * 1.414))
    return ALPHA*term_S + BETA*term_E + KAPPA*term_fair + ZETA*term_lq

class Simulation:
    def __init__(self, n_nodes=50):
        self.n_nodes = n_nodes
        self.rng = random.Random() # New random instance
        self.nodes = []
        self.clusters = defaultdict(list)
        self.current_heads = {}
        self.sim_time = 0
        self.gateway = (AREA_SIZE/2, AREA_SIZE/2)
        self.reset(n_nodes)

    def reset(self, n_nodes):
        self.n_nodes = int(n_nodes)
        self.sim_time = 0
        
        # Setup Network
        centers = [(self.rng.uniform(100, AREA_SIZE-100), self.rng.uniform(100, AREA_SIZE-100)) for _ in range(N_CLUSTERS)]
        self.nodes = []
        for i in range(self.n_nodes):
            c_idx = self.rng.randint(0, N_CLUSTERS-1)
            cx, cy = centers[c_idx]
            nx = cx + self.rng.gauss(0, 80)
            ny = cy + self.rng.gauss(0, 80)
            pos = (max(0, min(AREA_SIZE, nx)), max(0, min(AREA_SIZE, ny)))
            self.nodes.append(Node(i, pos, c_idx))

        self.clusters = defaultdict(list)
        for n in self.nodes: self.clusters[n.cluster].append(n)
        self.current_heads = {c: None for c in self.clusters}

    def step(self):
        # Run logic 1x per frame to slow down backend progression too, or keep it 3x? 
        # User said "simulation is going really fast", often better to slow down updates.
        # Let's reduce internal ticks to 1 per step call as well.
        for _ in range(1):
            self._run_simulation_step()
        
        return self.get_state()

    def _run_simulation_step(self):
        self.sim_time += 1
        ev_pos = (self.rng.uniform(0, AREA_SIZE), self.rng.uniform(0, AREA_SIZE))

        for n in self.nodes:
            if n.dead: continue
            n.consume(0.2, self.sim_time)
            n.S *= 0.9
            if not n.is_head: n.fair += 0.02
            if dist(n.pos, ev_pos) < 150: n.S += 0.8; n.consume(0.5, self.sim_time)

        for c_id, members in self.clusters.items():
            head = self.current_heads.get(c_id)
            if head is None or head.dead or (head and (self.sim_time - head.head_since) > T_MAX):
                candidates = [n for n in members if not n.dead]
                if candidates:
                    winner = max(candidates, key=lambda n: calculate_utility(n, self.gateway))
                    if head and head != winner: head.is_head = False
                    self.current_heads[c_id] = winner; winner.is_head = True
                    winner.head_since = self.sim_time; winner.fair = 0.0; winner.consume(1.5, self.sim_time)

    def get_state(self):
        # Return serializable state
        nodes_data = []
        links = []
        dead_nodes_stats = []
        
        for n in self.nodes:
            color = n.get_color()
            
            # Logic for links: if not head, link to head
            if not n.is_head and not n.dead:
                head = self.current_heads.get(n.cluster)
                if head and not head.dead:
                    links.append({
                        'start': n.pos,
                        'end': head.pos
                    })
            
            if n.dead:
                downtime = self.sim_time - n.dead_since if n.dead_since is not None else 0
                dead_nodes_stats.append({
                    'id': n.idx,
                    'dead_since': n.dead_since,
                    'downtime': downtime
                })

            nodes_data.append({
                'id': n.idx,
                'x': n.pos[0],
                'y': n.pos[1],
                'color': color,
                'is_head': n.is_head,
                'dead': n.dead,
                'batt': n.batt,
                'cluster': n.cluster
            })
            
        return {
            'sim_time': self.sim_time,
            'gateway': self.gateway,
            'nodes': nodes_data,
            'links': links,
            'dead_stats': dead_nodes_stats
        }