divagr1925 commited on
Commit
6ad7bd8
·
1 Parent(s): 2343011

Add reward handling and normalization features to environment

Browse files

- Introduced raw and normalized reward attributes in ClusterObservation and AntiAtroposEnvironment.
- Implemented reward normalization logic in stability module.
- Updated observability metrics to track raw and normalized rewards.
- Added tests for reward normalization and environment reward behavior.

client.py CHANGED
@@ -111,6 +111,9 @@ class AntiAtroposEnv(
111
  data_freshness_ms=obs_data.get("data_freshness_ms", 0),
112
  action_ack_status=obs_data.get("action_ack_status", "success"),
113
  choke_level=obs_data.get("choke_level", 0.0),
 
 
 
114
  done=payload.get("done", False),
115
  reward=payload.get("reward", 0.0),
116
  )
 
111
  data_freshness_ms=obs_data.get("data_freshness_ms", 0),
112
  action_ack_status=obs_data.get("action_ack_status", "success"),
113
  choke_level=obs_data.get("choke_level", 0.0),
114
+ raw_reward=obs_data.get("raw_reward", 0.0),
115
+ normalized_reward=obs_data.get("normalized_reward", 0.0),
116
+ reward_scale_version=obs_data.get("reward_scale_version", "sigmoid-v1"),
117
  done=payload.get("done", False),
118
  reward=payload.get("reward", 0.0),
119
  )
models.py CHANGED
@@ -155,6 +155,9 @@ class ClusterObservation(BaseModel):
155
  action_id: str = ""
156
  executor_latency_ms: float = Field(default=0.0, ge=0.0)
157
  executor_error_code: str = ""
 
 
 
158
  choke_level: float = 0.0
159
 
160
  nodes: list[NodeObservation]
 
155
  action_id: str = ""
156
  executor_latency_ms: float = Field(default=0.0, ge=0.0)
157
  executor_error_code: str = ""
158
+ raw_reward: float = 0.0
159
+ normalized_reward: float = Field(default=0.0, ge=0.0, le=1.0)
160
+ reward_scale_version: str = "sigmoid-v1"
161
  choke_level: float = 0.0
162
 
163
  nodes: list[NodeObservation]
server/AntiAtropos_environment.py CHANGED
@@ -1,5 +1,6 @@
1
  import time
2
  import json
 
3
  import logging
4
  from uuid import uuid4
5
 
@@ -9,13 +10,13 @@ from openenv.core.env_server.types import State
9
  try:
10
  from ..models import SREAction, ClusterObservation, NodeObservation, NodeStatus, EnvironmentMode
11
  from ..simulator import ClusterSimulator, COST_PER_CAPACITY_UNIT_PER_HOUR
12
- from ..stability import compute_lyapunov, compute_reward
13
  from ..telemetry import PrometheusClient, get_observability_tracker
14
  from ..control import KubernetesExecutor, ActionValidator
15
  except ImportError:
16
  from models import SREAction, ClusterObservation, NodeObservation, NodeStatus, EnvironmentMode # type: ignore[no-redef]
17
  from simulator import ClusterSimulator, COST_PER_CAPACITY_UNIT_PER_HOUR # type: ignore[no-redef]
18
- from stability import compute_lyapunov, compute_reward # type: ignore[no-redef]
19
  from telemetry import PrometheusClient, get_observability_tracker # type: ignore[no-redef]
20
  from control import KubernetesExecutor, ActionValidator # type: ignore[no-redef]
21
 
@@ -34,6 +35,7 @@ MAX_REQUEST_RATE_NORM = 100.0
34
 
35
  MAX_STEPS: int = 100 # Episode length
36
  N_NODES: int = 5 # Cluster size
 
37
 
38
 
39
  class AntiAtroposEnvironment(Environment):
@@ -70,6 +72,11 @@ class AntiAtroposEnvironment(Environment):
70
  self._last_action_id: str = ""
71
  self._last_executor_latency_ms: float = 0.0
72
  self._last_executor_error_code: str = ""
 
 
 
 
 
73
  self._last_metric_time: float = 0.0
74
 
75
  def reset(self, task_id: str = "task-1", mode: str = "simulated") -> ClusterObservation:
@@ -88,6 +95,8 @@ class AntiAtroposEnvironment(Environment):
88
  self._last_action_id = ""
89
  self._last_executor_latency_ms = 0.0
90
  self._last_executor_error_code = ""
 
 
91
 
92
  # Only set baseline metric time for hybrid/live to prevent misleading freshness in SIM
93
  if self._mode in [EnvironmentMode.HYBRID, EnvironmentMode.LIVE]:
@@ -123,6 +132,8 @@ class AntiAtroposEnvironment(Environment):
123
  self._last_action_id = str(uuid4())
124
  self._last_executor_latency_ms = 0.0
125
  self._last_executor_error_code = ""
 
 
126
 
127
  # 1. Action Validation & Execution
128
  valid_targets = [n["node_id"] for n in self._nodes_true]
@@ -196,7 +207,7 @@ class AntiAtroposEnvironment(Environment):
196
 
197
  # 7. Compute scalar reward
198
  cost = self._compute_cost(self._nodes_true)
199
- reward = compute_reward(
200
  v_prev=self._prev_lyapunov,
201
  v_curr=current_lyapunov,
202
  cost=cost,
@@ -205,6 +216,10 @@ class AntiAtroposEnvironment(Environment):
205
  beta=BETA,
206
  gamma=GAMMA
207
  )
 
 
 
 
208
 
209
  self._prev_lyapunov = current_lyapunov
210
 
@@ -225,7 +240,9 @@ class AntiAtroposEnvironment(Environment):
225
  action_type=str(action.action_type.value),
226
  target_node_id=str(action.target_node_id),
227
  ack_status=self._action_ack_status,
228
- reward=reward,
 
 
229
  lyapunov_energy=obs.lyapunov_energy,
230
  total_queue_backlog=obs.total_queue_backlog,
231
  average_latency_ms=obs.average_latency_ms,
@@ -248,7 +265,10 @@ class AntiAtroposEnvironment(Environment):
248
  "action_ack_status": self._action_ack_status,
249
  "executor_latency_ms": self._last_executor_latency_ms,
250
  "executor_error_code": self._last_executor_error_code,
251
- "reward": reward,
 
 
 
252
  "lyapunov_energy": obs.lyapunov_energy,
253
  "average_latency_ms_norm": obs.average_latency_ms,
254
  "total_queue_backlog_norm": obs.total_queue_backlog,
@@ -364,6 +384,9 @@ class AntiAtroposEnvironment(Environment):
364
  action_id=self._last_action_id,
365
  executor_latency_ms=self._last_executor_latency_ms,
366
  executor_error_code=self._last_executor_error_code,
 
 
 
367
  choke_level=0.0,
368
  done=False,
369
  reward=0.0,
 
1
  import time
2
  import json
3
+ import os
4
  import logging
5
  from uuid import uuid4
6
 
 
10
  try:
11
  from ..models import SREAction, ClusterObservation, NodeObservation, NodeStatus, EnvironmentMode
12
  from ..simulator import ClusterSimulator, COST_PER_CAPACITY_UNIT_PER_HOUR
13
+ from ..stability import compute_lyapunov, compute_reward, normalize_reward, REWARD_SCALE_VERSION
14
  from ..telemetry import PrometheusClient, get_observability_tracker
15
  from ..control import KubernetesExecutor, ActionValidator
16
  except ImportError:
17
  from models import SREAction, ClusterObservation, NodeObservation, NodeStatus, EnvironmentMode # type: ignore[no-redef]
18
  from simulator import ClusterSimulator, COST_PER_CAPACITY_UNIT_PER_HOUR # type: ignore[no-redef]
19
+ from stability import compute_lyapunov, compute_reward, normalize_reward, REWARD_SCALE_VERSION # type: ignore[no-redef]
20
  from telemetry import PrometheusClient, get_observability_tracker # type: ignore[no-redef]
21
  from control import KubernetesExecutor, ActionValidator # type: ignore[no-redef]
22
 
 
35
 
36
  MAX_STEPS: int = 100 # Episode length
37
  N_NODES: int = 5 # Cluster size
38
+ REWARD_OUTPUT_MODES = {"normalized", "raw"}
39
 
40
 
41
  class AntiAtroposEnvironment(Environment):
 
72
  self._last_action_id: str = ""
73
  self._last_executor_latency_ms: float = 0.0
74
  self._last_executor_error_code: str = ""
75
+ self._last_raw_reward: float = 0.0
76
+ self._last_normalized_reward: float = 0.0
77
+ self._reward_output_mode: str = os.getenv("ANTIATROPOS_REWARD_OUTPUT_MODE", "normalized").strip().lower()
78
+ if self._reward_output_mode not in REWARD_OUTPUT_MODES:
79
+ self._reward_output_mode = "normalized"
80
  self._last_metric_time: float = 0.0
81
 
82
  def reset(self, task_id: str = "task-1", mode: str = "simulated") -> ClusterObservation:
 
95
  self._last_action_id = ""
96
  self._last_executor_latency_ms = 0.0
97
  self._last_executor_error_code = ""
98
+ self._last_raw_reward = 0.0
99
+ self._last_normalized_reward = 0.0
100
 
101
  # Only set baseline metric time for hybrid/live to prevent misleading freshness in SIM
102
  if self._mode in [EnvironmentMode.HYBRID, EnvironmentMode.LIVE]:
 
132
  self._last_action_id = str(uuid4())
133
  self._last_executor_latency_ms = 0.0
134
  self._last_executor_error_code = ""
135
+ self._last_raw_reward = 0.0
136
+ self._last_normalized_reward = 0.0
137
 
138
  # 1. Action Validation & Execution
139
  valid_targets = [n["node_id"] for n in self._nodes_true]
 
207
 
208
  # 7. Compute scalar reward
209
  cost = self._compute_cost(self._nodes_true)
210
+ raw_reward = compute_reward(
211
  v_prev=self._prev_lyapunov,
212
  v_curr=current_lyapunov,
213
  cost=cost,
 
216
  beta=BETA,
217
  gamma=GAMMA
218
  )
219
+ normalized_reward = normalize_reward(raw_reward)
220
+ reward = normalized_reward if self._reward_output_mode == "normalized" else raw_reward
221
+ self._last_raw_reward = raw_reward
222
+ self._last_normalized_reward = normalized_reward
223
 
224
  self._prev_lyapunov = current_lyapunov
225
 
 
240
  action_type=str(action.action_type.value),
241
  target_node_id=str(action.target_node_id),
242
  ack_status=self._action_ack_status,
243
+ reward_output=reward,
244
+ reward_raw=raw_reward,
245
+ reward_normalized=normalized_reward,
246
  lyapunov_energy=obs.lyapunov_energy,
247
  total_queue_backlog=obs.total_queue_backlog,
248
  average_latency_ms=obs.average_latency_ms,
 
265
  "action_ack_status": self._action_ack_status,
266
  "executor_latency_ms": self._last_executor_latency_ms,
267
  "executor_error_code": self._last_executor_error_code,
268
+ "reward_output": reward,
269
+ "reward_raw": raw_reward,
270
+ "reward_normalized": normalized_reward,
271
+ "reward_output_mode": self._reward_output_mode,
272
  "lyapunov_energy": obs.lyapunov_energy,
273
  "average_latency_ms_norm": obs.average_latency_ms,
274
  "total_queue_backlog_norm": obs.total_queue_backlog,
 
384
  action_id=self._last_action_id,
385
  executor_latency_ms=self._last_executor_latency_ms,
386
  executor_error_code=self._last_executor_error_code,
387
+ raw_reward=self._last_raw_reward,
388
+ normalized_reward=self._last_normalized_reward,
389
+ reward_scale_version=REWARD_SCALE_VERSION,
390
  choke_level=0.0,
391
  done=False,
392
  reward=0.0,
stability.py CHANGED
@@ -38,6 +38,7 @@ Key concepts implemented
38
 
39
  from __future__ import annotations
40
 
 
41
  import math
42
  import statistics
43
  from typing import Sequence
@@ -57,6 +58,16 @@ STABILITY_WINDOW: int = 10
57
  trend-stable (V is on a decreasing trajectory)."""
58
 
59
 
 
 
 
 
 
 
 
 
 
 
60
  # ---------------------------------------------------------------------------
61
  # Core Lyapunov functions
62
  # ---------------------------------------------------------------------------
@@ -264,3 +275,25 @@ def compute_reward(
264
  """
265
  delta_v = compute_drift(v_prev, v_curr)
266
  return -(alpha * delta_v + beta * cost + gamma * sla_violation_step)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  from __future__ import annotations
40
 
41
+ import os
42
  import math
43
  import statistics
44
  from typing import Sequence
 
58
  trend-stable (V is on a decreasing trajectory)."""
59
 
60
 
61
+ # ---------------------------------------------------------------------------
62
+ # Reward normalisation defaults (env-overridable)
63
+ # ---------------------------------------------------------------------------
64
+
65
+ REWARD_NORM_MIDPOINT: float = float(os.getenv("ANTIATROPOS_REWARD_MIDPOINT", "0.0"))
66
+ REWARD_NORM_TEMPERATURE: float = float(os.getenv("ANTIATROPOS_REWARD_TEMPERATURE", "5.0"))
67
+ REWARD_NORM_EPS: float = float(os.getenv("ANTIATROPOS_REWARD_EPS", "1e-8"))
68
+ REWARD_SCALE_VERSION: str = "sigmoid-v1"
69
+
70
+
71
  # ---------------------------------------------------------------------------
72
  # Core Lyapunov functions
73
  # ---------------------------------------------------------------------------
 
275
  """
276
  delta_v = compute_drift(v_prev, v_curr)
277
  return -(alpha * delta_v + beta * cost + gamma * sla_violation_step)
278
+
279
+
280
+ def normalize_reward(
281
+ raw_reward: float,
282
+ midpoint: float = REWARD_NORM_MIDPOINT,
283
+ temperature: float = REWARD_NORM_TEMPERATURE,
284
+ eps: float = REWARD_NORM_EPS,
285
+ ) -> float:
286
+ """
287
+ Deterministically map raw reward to [0, 1] using a smooth sigmoid.
288
+
289
+ reward_01 = 1 / (1 + exp(-(raw_reward - midpoint) / temperature))
290
+ """
291
+ temp = max(float(eps), abs(float(temperature)))
292
+ z = (float(raw_reward) - float(midpoint)) / temp
293
+ if z >= 0:
294
+ exp_neg = math.exp(-z)
295
+ out = 1.0 / (1.0 + exp_neg)
296
+ else:
297
+ exp_pos = math.exp(z)
298
+ out = exp_pos / (1.0 + exp_pos)
299
+ return min(1.0, max(0.0, float(out)))
telemetry/observability.py CHANGED
@@ -47,7 +47,17 @@ class ObservabilityTracker:
47
 
48
  self.reward_gauge = Gauge(
49
  "antiatropos_reward",
50
- "Latest reward value",
 
 
 
 
 
 
 
 
 
 
51
  ["task_id", "mode"],
52
  )
53
  self.lyapunov_gauge = Gauge(
@@ -73,7 +83,9 @@ class ObservabilityTracker:
73
  action_type: str,
74
  target_node_id: str,
75
  ack_status: str,
76
- reward: float,
 
 
77
  lyapunov_energy: float,
78
  total_queue_backlog: float,
79
  average_latency_ms: float,
@@ -93,7 +105,9 @@ class ObservabilityTracker:
93
  target_node_id=target_node_id,
94
  ack_class=ack_class,
95
  ).inc()
96
- self.reward_gauge.labels(task_id=task_id, mode=mode).set(float(reward))
 
 
97
  self.lyapunov_gauge.labels(task_id=task_id, mode=mode).set(float(lyapunov_energy))
98
  self.queue_gauge.labels(task_id=task_id, mode=mode).set(float(total_queue_backlog))
99
  self.latency_gauge.labels(task_id=task_id, mode=mode).set(float(average_latency_ms))
 
47
 
48
  self.reward_gauge = Gauge(
49
  "antiatropos_reward",
50
+ "Latest output reward value (depends on reward output mode)",
51
+ ["task_id", "mode"],
52
+ )
53
+ self.reward_raw_gauge = Gauge(
54
+ "antiatropos_reward_raw",
55
+ "Latest raw reward value before normalization",
56
+ ["task_id", "mode"],
57
+ )
58
+ self.reward_normalized_gauge = Gauge(
59
+ "antiatropos_reward_normalized",
60
+ "Latest normalized reward value in [0,1]",
61
  ["task_id", "mode"],
62
  )
63
  self.lyapunov_gauge = Gauge(
 
83
  action_type: str,
84
  target_node_id: str,
85
  ack_status: str,
86
+ reward_output: float,
87
+ reward_raw: float,
88
+ reward_normalized: float,
89
  lyapunov_energy: float,
90
  total_queue_backlog: float,
91
  average_latency_ms: float,
 
105
  target_node_id=target_node_id,
106
  ack_class=ack_class,
107
  ).inc()
108
+ self.reward_gauge.labels(task_id=task_id, mode=mode).set(float(reward_output))
109
+ self.reward_raw_gauge.labels(task_id=task_id, mode=mode).set(float(reward_raw))
110
+ self.reward_normalized_gauge.labels(task_id=task_id, mode=mode).set(float(reward_normalized))
111
  self.lyapunov_gauge.labels(task_id=task_id, mode=mode).set(float(lyapunov_energy))
112
  self.queue_gauge.labels(task_id=task_id, mode=mode).set(float(total_queue_backlog))
113
  self.latency_gauge.labels(task_id=task_id, mode=mode).set(float(average_latency_ms))