Ridreb05 commited on
Commit
075db57
·
verified ·
1 Parent(s): 5380a94

Update simulation/reward_calculator.py

Browse files
Files changed (1) hide show
  1. simulation/reward_calculator.py +42 -207
simulation/reward_calculator.py CHANGED
@@ -1,220 +1,55 @@
1
- """
2
- Reward Calculator
3
- =================
4
- 5-component reward function for spacecraft proximity operations.
5
-
6
- Design principles (from hackathon self-serve guide):
7
- 1. Rich informative signal at every step — not just 0/1 at episode end
8
- 2. Multiple independent reward functions — harder to hack
9
- 3. Wide spread between good (+4 to +8) and bad (−2) episodes for GRPO
10
- 4. Hard to game — agent cannot get high reward without solving the real task
11
-
12
- Component summary:
13
- R_approach +0.0 to +2.0/step Dense: proportional to distance reduction
14
- R_fuel −0.05/unit thrust Penalise wasteful burns
15
- R_los −0.5 if violated LOS cone constraint (±45° at <50m)
16
- R_blackout +0.3 bonus/step Sensible decisions during comms blackout
17
- R_terminal +4 to +8 or −2 Sparse: docking success or failure
18
- R_reasoning +0.1 if present Bonus for structured reasoning text
19
- """
20
-
21
- from dataclasses import dataclass
22
- from typing import Optional
23
  import numpy as np
24
- from simulation.cwh_dynamics import CWHState, ThrustAction, DOCKING_DISTANCE_M
25
-
26
-
27
- # ---------------------------------------------------------------------------
28
- # Reward weights — tune these for GRPO convergence
29
- # ---------------------------------------------------------------------------
30
- W_APPROACH: float = 2.0 # max per-step approach reward
31
- W_FUEL_PENALTY: float = 0.05 # per kg of fuel consumed
32
- W_LOS_PENALTY: float = 0.5 # per LOS violation
33
- W_BLACKOUT_BONUS: float = 0.3 # per step during blackout (if reasonable action)
34
- W_TERMINAL_SUCCESS: float = 5.0 # base terminal success reward
35
- W_TERMINAL_BONUS_FUEL: float = 3.0 # bonus for fuel conservation at docking
36
- W_TERMINAL_BONUS_SPEED: float = 2.0 # bonus for gentle approach speed
37
- W_TERMINAL_FAILURE: float = -2.0 # timeout / crash / abort
38
- W_FORMAT_BONUS: float = 0.1 # agent provided structured reasoning
39
-
40
- # Bounds for GRPO — must have spread
41
- REWARD_EPISODE_MIN: float = -3.0
42
- REWARD_EPISODE_MAX: float = 10.0
43
-
44
 
45
  @dataclass
46
- class RewardBreakdown:
47
- """Detailed reward decomposition — logged during training for diagnostics."""
48
  approach: float = 0.0
49
  fuel: float = 0.0
50
  los: float = 0.0
51
  blackout: float = 0.0
52
  terminal: float = 0.0
53
- reasoning: float = 0.0
54
-
55
- @property
56
- def total(self) -> float:
57
- return (self.approach + self.fuel + self.los +
58
- self.blackout + self.terminal + self.reasoning)
59
-
60
- def to_dict(self) -> dict:
61
- return {
62
- "r_approach": round(self.approach, 4),
63
- "r_fuel": round(self.fuel, 4),
64
- "r_los": round(self.los, 4),
65
- "r_blackout": round(self.blackout, 4),
66
- "r_terminal": round(self.terminal, 4),
67
- "r_reasoning": round(self.reasoning, 4),
68
- "r_total": round(self.total, 4),
69
- }
70
-
71
 
72
  class RewardCalculator:
73
- """
74
- Computes step and episode rewards for spacecraft proximity operations.
75
-
76
- GRPO needs variance. This calculator is designed to produce:
77
- - Warm-start easy scenarios: +1.5 to +5.0 range
78
- - Standard successful episode: +4.0 to +8.0
79
- - Failed episode (timeout): −2.0 to +0.5
80
- - Crashed episode: −2.0 to −1.0
81
- """
82
-
83
  def __init__(self):
84
- self._episode_cumulative = RewardBreakdown()
85
-
86
  def reset(self):
87
- """Call at the start of each episode."""
88
- self._episode_cumulative = RewardBreakdown()
89
-
90
- def step_reward(
91
- self,
92
- prev_state: CWHState,
93
- new_state: CWHState,
94
- action: ThrustAction,
95
- info: dict,
96
- reasoning_text: Optional[str] = None,
97
- ) -> RewardBreakdown:
98
- """
99
- Compute step-level reward components.
100
- Called after every environment step.
101
- """
102
- rb = RewardBreakdown()
103
-
104
- # ── Component 1: Approach Progress ────────────────────────
105
- # Reward proportional to fractional distance reduction
106
- # Normalised so maximum per-step reward is W_APPROACH
107
- prev_dist = prev_state.distance
108
- curr_dist = new_state.distance
109
- if prev_dist > 1e-6:
110
- distance_reduction = (prev_dist - curr_dist) / prev_dist
111
- # Clip: no reward for moving away, max reward for rapid approach
112
- rb.approach = float(np.clip(distance_reduction * W_APPROACH, -0.3, W_APPROACH))
 
 
 
 
 
 
 
113
  else:
114
- rb.approach = 0.0
115
-
116
- # ── Component 2: Fuel Efficiency ──────────────────────────
117
- # Penalise fuel consumption per step
118
- fuel_consumed = info.get("fuel_consumed_kg", 0.0)
119
- rb.fuel = -W_FUEL_PENALTY * fuel_consumed * 10.0 # scale to sensible range
120
-
121
- # ── Component 3: LOS Constraint ───────────────────────────
122
- if info.get("los_violation", False):
123
- rb.los = -W_LOS_PENALTY
124
-
125
- # ── Component 4: Comms Blackout Handling ──────────────────
126
- # Bonus if agent makes a reasonable decision during blackout
127
- # "Reasonable" = small thrust magnitude (not panicking) or holding course
128
- if not prev_state.comms_active:
129
- thrust_mag = action.magnitude
130
- if thrust_mag < 0.5: # conservative action during blackout = good
131
- rb.blackout = W_BLACKOUT_BONUS
132
- elif thrust_mag < 1.0:
133
- rb.blackout = W_BLACKOUT_BONUS * 0.5
134
- # Large thrust during blackout = no bonus (blind firing)
135
-
136
- # ── Component 6: Reasoning Quality ────────────────────────
137
- if reasoning_text and len(reasoning_text.strip()) > 20:
138
- rb.reasoning = W_FORMAT_BONUS
139
-
140
- # Accumulate
141
- self._episode_cumulative.approach += rb.approach
142
- self._episode_cumulative.fuel += rb.fuel
143
- self._episode_cumulative.los += rb.los
144
- self._episode_cumulative.blackout += rb.blackout
145
- self._episode_cumulative.reasoning += rb.reasoning
146
-
147
- return rb
148
-
149
- def terminal_reward(
150
- self,
151
- final_state: CWHState,
152
- info: dict,
153
- ) -> RewardBreakdown:
154
- """
155
- Compute terminal reward at episode end.
156
- This is the biggest signal — dominates GRPO advantage estimation.
157
- """
158
- rb = RewardBreakdown()
159
-
160
- if final_state.docked:
161
- # Base docking reward
162
- rb.terminal = W_TERMINAL_SUCCESS
163
-
164
- # Bonus for fuel conservation (proportional to remaining fuel)
165
- fuel_ratio = final_state.fuel_kg / 10.0 # normalise to initial budget assumption
166
- rb.terminal += W_TERMINAL_BONUS_FUEL * min(fuel_ratio, 1.0)
167
-
168
- # Bonus for gentle closing velocity at docking
169
- speed = final_state.speed
170
- if speed < 0.02:
171
- rb.terminal += W_TERMINAL_BONUS_SPEED # perfect approach
172
- elif speed < 0.05:
173
- rb.terminal += W_TERMINAL_BONUS_SPEED * 0.5 # good approach
174
- # Above DOCKING_VELOCITY_MS threshold: no speed bonus
175
-
176
- elif final_state.mission_aborted:
177
- # Crash / out of bounds / fuel exhausted far from target
178
- rb.terminal = W_TERMINAL_FAILURE
179
-
180
- # Partial credit if they got close before failing
181
- dist = final_state.distance
182
- if dist < 10.0:
183
- rb.terminal += 0.5 # got close, then something went wrong
184
- elif dist < 30.0:
185
- rb.terminal += 0.2
186
-
187
- else:
188
- # Timeout — ran out of steps
189
- rb.terminal = W_TERMINAL_FAILURE
190
-
191
- # Partial credit based on final distance
192
- dist = final_state.distance
193
- initial_dist = final_state.prev_distance # best proxy for initial
194
- if dist < 2.0:
195
- rb.terminal += 1.8 # basically docked, just didn't meet velocity
196
- elif dist < 10.0:
197
- rb.terminal += 1.2
198
- elif dist < 30.0:
199
- rb.terminal += 0.6
200
- elif dist < 50.0:
201
- rb.terminal += 0.2
202
-
203
- self._episode_cumulative.terminal += rb.terminal
204
- return rb
205
-
206
- def episode_summary(self) -> dict:
207
- """Return full episode reward breakdown for logging."""
208
- return {
209
- **self._episode_cumulative.to_dict(),
210
- "r_episode_total": round(self._episode_cumulative.total, 4),
211
- }
212
-
213
- @staticmethod
214
- def normalize_for_grpo(reward: float) -> float:
215
- """
216
- Normalize episode reward to a range suitable for GRPO.
217
- GRPO works best when rewards are in roughly [-3, +10] range.
218
- Clamp to prevent outlier episodes from dominating gradients.
219
- """
220
- return float(np.clip(reward, REWARD_EPISODE_MIN, REWARD_EPISODE_MAX))
 
1
+ """Reward function for GRPO training"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import numpy as np
3
+ from dataclasses import dataclass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  @dataclass
6
+ class RewardBundle:
 
7
  approach: float = 0.0
8
  fuel: float = 0.0
9
  los: float = 0.0
10
  blackout: float = 0.0
11
  terminal: float = 0.0
12
+ total: float = 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  class RewardCalculator:
 
 
 
 
 
 
 
 
 
 
15
  def __init__(self):
16
+ self.prev_dist = None
17
+
18
  def reset(self):
19
+ self.prev_dist = None
20
+
21
+ def step_reward(self, prev_state, new_state, action, info, reasoning_text=''):
22
+ bundle = RewardBundle()
23
+
24
+ # Approach progress
25
+ if self.prev_dist is None:
26
+ self.prev_dist = prev_state.distance
27
+ progress = self.prev_dist - new_state.distance
28
+ bundle.approach = np.clip(progress * 2.0, -0.5, 2.0)
29
+ self.prev_dist = new_state.distance
30
+
31
+ # Fuel penalty
32
+ thrust_mag = np.sqrt(action.fx**2 + action.fy**2)
33
+ bundle.fuel = -0.05 * thrust_mag
34
+
35
+ # LOS violation penalty
36
+ if new_state.los_violation:
37
+ bundle.los = -0.5
38
+
39
+ # Blackout bonus
40
+ if not new_state.comms_active:
41
+ bundle.blackout = 0.3
42
+
43
+ bundle.total = bundle.approach + bundle.fuel + bundle.los + bundle.blackout
44
+ return bundle
45
+
46
+ def terminal_reward(self, state, info):
47
+ bundle = RewardBundle()
48
+ if state.docked:
49
+ # Docking success bonus scaled by fuel remaining
50
+ fuel_pct = state.fuel_kg / 80.0
51
+ bundle.terminal = 5.0 + 5.0 * fuel_pct
52
  else:
53
+ bundle.terminal = -2.0
54
+ bundle.total = bundle.terminal
55
+ return bundle