krishuggingface commited on
Commit
01f8cd5
·
1 Parent(s): a8f21d0

Refactor: Restore intrinsic detector to fallback logic, rewrite README.md, and polish all codebase comments for final submission

Browse files
Files changed (7) hide show
  1. .env.example +10 -0
  2. README.md +81 -73
  3. inference.py +43 -9
  4. requirements.txt +1 -0
  5. src/attacks.py +2 -2
  6. src/env.py +5 -6
  7. src/graders.py +3 -3
.env.example ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # LLM Configuration
2
+ USE_LLM=1
3
+ API_BASE_URL=https://router.huggingface.co/v1
4
+ MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
5
+ # Get your token from https://huggingface.co/settings/tokens
6
+ HF_TOKEN=your_huggingface_token_here
7
+
8
+ # Environment Configuration
9
+ # Use http://0.0.0.0:7860 or http://localhost:7860 locally
10
+ ENV_URL=http://localhost:7860
README.md CHANGED
@@ -18,137 +18,145 @@ pinned: false
18
 
19
  ## Overview
20
 
21
- Phase-Locked Loops (PLLs) are critical components in grid-connected power converters that synchronize the inverter's output with the utility grid. The Synchronous Reference Frame PLL (SRF-PLL) estimates grid frequency and phase angle by tracking the q-axis voltage component making it a high-value target for **False Data Injection (FDI)** cyberattacks.
22
 
23
- This OpenEnv environment simulates an SRF-PLL under various FDI attack scenarios. An AI agent monitors time-windowed sensor observations (voltages, frequency deviations) and must detect, classify, and respond to attacks in real time before they cause loss of grid synchronization.
24
 
25
  ## Architecture
26
 
27
- ```
 
 
28
  Grid Voltage (50Hz)
29
 
30
 
31
- [FDI Attack Injection] ◄── Attacker injects false signal on va
32
 
33
 
34
  Clarke Transform (αβ)
35
 
36
 
37
- Park Transform (dq) ◄── uses estimated angle θ̂
38
 
39
 
40
- PI Controller ──► ω̂, θ̂ updated
41
 
42
 
43
- Agent observes: vq_window, omega_deviation_window, raw_voltages
44
 
45
 
46
- Agent outputs: attack_detected, attack_type, confidence
47
  ```
48
 
49
- ## Inference & Detection Strategy
50
 
51
- The environment natively features an **Adaptive Physics-Informed Detector** (`src/detector.py`) that calibrates anomaly residuals (R1, R3, R4, R5) during the PLL warm-up phase to identify stealthy voltage and frequency deviations.
52
 
53
- The default inference client (`inference.py`) deploys a **Smart Blending Agent** strategy:
54
- 1. It relies primarily on the environment's `AdaptiveDetector` output passed via `info["detector"]`.
55
- 2. As a **safety net**, if the detector's classification confidence drops below 50% (`< 0.5`) on ambiguous anomalies, the client dynamically falls back to an independent, cumulative **Rule-Based Heuristic Agent**.
56
- 3. Optionally, an LLM agent (e.g., `Qwen/Qwen2.5-72B-Instruct`) can be enabled natively via the `USE_LLM=1` environment variable.
 
 
 
 
 
57
 
58
  ## Tasks
59
 
60
- | Task | ID | Difficulty | Attack Type | Objective | Score |
 
 
61
  |------|----|-----------|-------------|-----------|-------|
62
- | Sinusoidal FDI Detection | 0 | Easy | Sinusoidal injection | Detect within 100 steps | Time-based decay |
63
- | Multi-Attack Classification | 1 | Medium | Sinusoidal/Ramp/Pulse | Classify attack type | Accuracy + speed |
64
- | Stealthy Attack Detection | 2 | Hard | Low-amplitude phase drift | Detect before lock loss | Prevention score |
65
 
66
  ## Observation Space
67
 
68
- Each step provides a JSON observation with the following fields:
69
 
70
  | Field | Shape | Description |
71
  |-------|-------|-------------|
72
- | `vq_window` | `[20]` | q-axis voltage error signal (pu) |
73
- | `vd_window` | `[20]` | d-axis voltage (pu) |
74
- | `omega_window` | `[20]` | Normalized frequency deviation from nominal |
75
- | `omega_deviation_window` | `[20]` | Frequency deviation from nominal (rad/s) |
76
- | `raw_voltages` | `[3]` | Raw three-phase voltages `[va, vb, vc]` (pu) |
77
- | `step` | scalar | Current simulation step |
78
- | `task_id` | scalar | Task identifier (0, 1, or 2) |
79
 
80
- **Total observation dimension**: 83 (20+20+20+20+3)
81
 
82
  ## Action Space
83
 
84
- Agents return a JSON action each step:
85
 
86
  | Field | Type | Range | Description |
87
  |-------|------|-------|-------------|
88
- | `attack_detected` | `bool` | — | Whether an attack is detected |
89
- | `attack_type` | `int` | 0–4 | 0=none, 1=sinusoidal, 2=ramp, 3=pulse, 4=stealthy |
90
- | `confidence` | `float` | 0.0–1.0 | Agent's confidence in its classification |
91
- | `protective_action` | `int` | 0–3 | 0=none, 1=alert, 2=reduce power, 3=disconnect |
92
 
93
- ## API Endpoints
94
 
95
- ### Reset Environment
96
- ```bash
97
- curl -X POST http://localhost:7860/reset \
98
- -H "Content-Type: application/json" \
99
- -d '{"task_id": 0, "seed": 42}'
100
- ```
101
 
102
- ### Step
103
- ```bash
104
- curl -X POST http://localhost:7860/step \
105
- -H "Content-Type: application/json" \
106
- -d '{"attack_detected": false, "attack_type": 0, "confidence": 0.5, "protective_action": 0}'
107
- ```
108
 
109
- ### Get State
110
  ```bash
111
- curl http://localhost:7860/state
 
112
  ```
113
 
114
- ### Health Check
115
  ```bash
116
- curl http://localhost:7860/health
 
117
  ```
118
 
119
- ## Quick Start
120
 
121
- ### With Docker
122
 
123
- ```bash
124
- docker build -t pll-cyberattack-env .
125
- docker run -p 7860:7860 pll-cyberattack-env
126
- ```
 
 
127
 
128
- ### Without Docker
129
 
130
- ```bash
131
- pip install -r requirements.txt
132
- uvicorn src.api:app --host 0.0.0.0 --port 7860
133
- ```
 
 
 
134
 
135
- ## Environment Variables
 
 
 
 
 
 
136
 
137
- | Variable | Required | Default | Description |
138
- |----------|----------|---------|-------------|
139
- | `API_BASE_URL` | No | `https://router.huggingface.co/v1` | LLM API endpoint |
140
- | `MODEL_NAME` | No | `Qwen/Qwen2.5-72B-Instruct` | Model identifier |
141
- | `HF_TOKEN` | Yes | — | HuggingFace API token |
142
 
143
  ## Baseline Performance
144
 
145
- The default hybrid strategy (Adaptive Detector + Heuristic Fallback) achieves the following baseline scores evaluated locally over 500-step episodes:
146
 
147
  * **Task 0 (Sinusoidal FDI):** 1.0000
148
- * **Task 1 (Multi-Attack Classification):** 0.8720
149
- * **Task 2 (Stealthy Drift):** 0.1639
150
- * **Average Score:** `0.6786`
151
 
152
- ## Live Demo
153
-
154
- 🚀 **HuggingFace Space**: [https://huggingface.co/spaces/krishuggingface/CyberAttack-PLL](https://huggingface.co/spaces/krishuggingface/CyberAttack-PLL)
 
18
 
19
  ## Overview
20
 
21
+ Phase-Locked Loops (PLLs) are critical components in grid-connected power converters, responsible for synchronizing the inverter's output with the utility grid. The Synchronous Reference Frame PLL (SRF-PLL) estimates grid frequency and phase angle by tracking the q-axis voltage component. Because of its critical role and reliance on sensor data, the SRF-PLL is a high-value target for **False Data Injection (FDI)** cyberattacks.
22
 
23
+ This OpenEnv environment simulates an SRF-PLL subjected to varied FDI attack scenarios. An AI agent acts as a cyber-guard: it monitors arriving time-windowed sensor observations—such as voltages and frequency deviationsand must accurately detect, classify, and mitigate attacks in real-time before grid synchronization is lost.
24
 
25
  ## Architecture
26
 
27
+ The environment relies on a discrete-time SRF-PLL simulation running at a 1 ms step size. A streamlined view of the signal flow is below:
28
+
29
+ ```text
30
  Grid Voltage (50Hz)
31
 
32
 
33
+ [FDI Attack Injection] ◄── Attacker injects a malicious signal on phase `va`
34
 
35
 
36
  Clarke Transform (αβ)
37
 
38
 
39
+ Park Transform (dq) ◄── Uses the currently estimated angle θ̂
40
 
41
 
42
+ PI Controller ──► ω̂, θ̂ are updated continuously
43
 
44
 
45
+ Agent Observation ──► Agent receives: `vq_window`, `omega_deviation_window`, `raw_voltages`
46
 
47
 
48
+ Agent Action ──► Agent outputs: `attack_detected`, `attack_type`, `confidence`
49
  ```
50
 
51
+ ## Inference Flow & Detector Walkthrough
52
 
53
+ To balance speed and accuracy across thousands of steps, the standard inference client (`inference.py`) deploys a **Smart Blending Strategy**:
54
 
55
+ 1. **Environment Simulation (`env.py`)**:
56
+ Every step, the PLL updates its internal math based on potential attack injections. It yields a rich observation window of the last 20 frames for variables like $V_q$ and $\omega_{dev}$.
57
+ 2. **Adaptive Physics-Informed Detector (`src/detector.py`)**:
58
+ Before returning the observation to the client, the environment evaluates the data using an intrinsic physics-based detector. This detector calibrates anomaly residuals during the first 20 "healthy" warm-up steps. It tracks variances and symmetry to identify stealthy voltage anomalies, providing a baseline `confidence` score.
59
+ 3. **Smart Blending Client (`inference.py`)**:
60
+ The client receives the observation and the detector's baseline prediction.
61
+ * If the intrinsic detector has high confidence (> 50%), the client adopts its recommendation.
62
+ * If the anomaly is ambiguous (confidence < 50%), the client queries its own **Rule-Based Heuristic Agent**, which monitors historical $V_q$ growth, monotonicity, and zero-crossing density.
63
+ * *Optional*: If `USE_LLM=1` is set, the client uses an LLM (e.g., `Qwen2.5-72B`) for advanced reasoning. A resilient "circuit breaker" automatically transitions to the heuristic model if network or authentication failures occur.
64
 
65
  ## Tasks
66
 
67
+ The environment supports three sequentially evaluated difficulty levels:
68
+
69
+ | Task | ID | Difficulty | Attack Type | Objective | Score Metric |
70
  |------|----|-----------|-------------|-----------|-------|
71
+ | Sinusoidal FDI | 0 | Easy | Sinusoidal Injection | Detect attack within 100 steps of initiation. | Time-decaying detection reward. |
72
+ | Multi-Attack Class. | 1 | Medium | Sinusoidal, Ramp, Pulse | Safely and correctly classify the specific attack type. | Accuracy and speed aggregate. |
73
+ | Stealthy Detection | 2 | Hard | Low-amplitude phase drift | Detect slow deviations before the PLL loses lock (θ_error > 5°). | Preventative lock-loss metric. |
74
 
75
  ## Observation Space
76
 
77
+ At each step, the environment provides a JSON observation containing:
78
 
79
  | Field | Shape | Description |
80
  |-------|-------|-------------|
81
+ | `vq_window` | `[20]` | q-axis voltage error signal (pu). |
82
+ | `vd_window` | `[20]` | d-axis voltage (pu). |
83
+ | `omega_window` | `[20]` | Normalized frequency deviation from nominal. |
84
+ | `omega_deviation_window` | `[20]` | Frequency deviation from nominal (rad/s). |
85
+ | `raw_voltages` | `[3]` | Raw three-phase voltages `[va, vb, vc]` (pu). |
86
+ | `step` | `scalar` | Current simulation time step. |
87
+ | `task_id` | `scalar` | Current task identifier (0, 1, or 2). |
88
 
89
+ **Total observation dimension**: 83 ($20 \times 4 + 3$)
90
 
91
  ## Action Space
92
 
93
+ Agents must return a structured JSON response predicting the system state:
94
 
95
  | Field | Type | Range | Description |
96
  |-------|------|-------|-------------|
97
+ | `attack_detected` | `bool` | — | True if malicious injection is suspected. |
98
+ | `attack_type` | `int` | 0–4 | 0=None, 1=Sinusoidal, 2=Ramp, 3=Pulse, 4=Stealthy. |
99
+ | `confidence` | `float` | 0.0–1.0 | Absolute predictive certainty. |
100
+ | `protective_action` | `int` | 0–3 | Suggested mitigation: 0=None, 1=Alert, 2=Reduce Power, 3=Disconnect. |
101
 
102
+ ## Setup & API Usage
103
 
104
+ The system acts as a standard REST API server over port `7860`.
 
 
 
 
 
105
 
106
+ ### Local Setup
 
 
 
 
 
107
 
108
+ **Via Python (Recommended)**:
109
  ```bash
110
+ pip install -r requirements.txt
111
+ uvicorn src.api:app --host 0.0.0.0 --port 7860
112
  ```
113
 
114
+ **Via Docker**:
115
  ```bash
116
+ docker build -t pll-cyberattack-env .
117
+ docker run -p 7860:7860 pll-cyberattack-env
118
  ```
119
 
120
+ ### Environment Variables
121
 
122
+ Configure execution behavior locally via a `.env` file (see `.env.example`).
123
 
124
+ | Variable | Default | Description |
125
+ |----------|---------|-------------|
126
+ | `API_BASE_URL` | `https://router.huggingface.co/v1` | Custom endpoint for Language Models. |
127
+ | `MODEL_NAME` | `Qwen/Qwen2.5-72B-Instruct` | Internal Model identifier. |
128
+ | `HF_TOKEN` | — | HuggingFace or valid proxy API key. |
129
+ | `USE_LLM` | `1` | Set to `1` to run the active LLM agent, `0` for pure heuristics. |
130
 
131
+ ### REST Endpoints
132
 
133
+ 1. **POST `/reset`**
134
+ Initializes the environment for a specific task.
135
+ ```bash
136
+ curl -X POST http://localhost:7860/reset \
137
+ -H "Content-Type: application/json" \
138
+ -d '{"task_id": 0, "seed": 42}'
139
+ ```
140
 
141
+ 2. **POST `/step`**
142
+ Submit an action based on recent observations and advance by one tick.
143
+ ```bash
144
+ curl -X POST http://localhost:7860/step \
145
+ -H "Content-Type: application/json" \
146
+ -d '{"attack_detected": false, "attack_type": 0, "confidence": 0.5, "protective_action": 0}'
147
+ ```
148
 
149
+ 3. **GET `/health`**
150
+ Returns operational status and step numbers.
 
 
 
151
 
152
  ## Baseline Performance
153
 
154
+ The default hybrid strategy outlined in `inference.py` consistently yields the following evaluation bounds across a full 500-step envelope:
155
 
156
  * **Task 0 (Sinusoidal FDI):** 1.0000
157
+ * **Task 1 (Multi-Attack Classification):** ~0.8720
158
+ * **Task 2 (Stealthy Drift):** ~0.1639
159
+ * **Aggregate System Average:** `0.6786`
160
 
161
+ ---
162
+ 🚀 **Live Environment Hosted on HuggingFace Spaces**: [krishuggingface/CyberAttack-PLL](https://huggingface.co/spaces/krishuggingface/CyberAttack-PLL)
 
inference.py CHANGED
@@ -21,6 +21,12 @@ import requests
21
  from typing import List, Optional
22
  from openai import OpenAI
23
 
 
 
 
 
 
 
24
  # ── Config — always read from environment, never hardcode ─────────────────────
25
  # The judging sandbox injects API_BASE_URL and API_KEY via their LiteLLM proxy.
26
  # All LLM calls MUST go through these values or the submission will be rejected.
@@ -106,7 +112,35 @@ def log_end(success: bool, steps: int, score: float, rewards: List[float]) -> No
106
  flush=True,
107
  )
108
 
109
- # ── Heuristic agent (FALLBACK ONLY used when LLM call fails) ────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  class HeuristicState:
112
  """Tracks running state for the heuristic agent across steps."""
@@ -301,15 +335,15 @@ def format_observation(obs: dict) -> str:
301
  _llm_disabled = False # circuit breaker — flips True after first LLM failure
302
 
303
 
304
- def llm_agent(obs: dict) -> dict:
305
  """Primary agent — calls the LLM through the injected proxy.
306
- Falls back to heuristic only if the API call itself raises an exception.
307
  Uses a circuit breaker: after the first failure, all future calls skip the
308
- network request and go straight to heuristic (restoring ~10s runtime).
309
  """
310
  global _llm_disabled
311
  if _llm_disabled:
312
- return heuristic_agent(obs)
313
 
314
  try:
315
  completion = client.chat.completions.create(
@@ -324,9 +358,9 @@ def llm_agent(obs: dict) -> dict:
324
  )
325
  return parse_llm_response(completion.choices[0].message.content or "")
326
  except Exception as e:
327
- print(f"[DEBUG] LLM error ({type(e).__name__}: {e}), disabling LLM for remaining steps", file=sys.stderr, flush=True)
328
  _llm_disabled = True
329
- return heuristic_agent(obs)
330
 
331
  # ── Episode runner ────────────────────────────────────────────────────────────
332
 
@@ -362,9 +396,9 @@ def run_episode(task_id: int) -> float:
362
  # This caps LLM calls at ~150 total across 3 tasks, keeping runtime
363
  # well under the 20-min judging limit even with 3s/call latency.
364
  if step_count % 10 == 0:
365
- action = llm_agent(obs)
366
  else:
367
- action = heuristic_agent(obs)
368
 
369
  step_resp = _session.post(
370
  f"{ENV_URL}/step",
 
21
  from typing import List, Optional
22
  from openai import OpenAI
23
 
24
+ try:
25
+ from dotenv import load_dotenv
26
+ load_dotenv()
27
+ except ImportError:
28
+ pass
29
+
30
  # ── Config — always read from environment, never hardcode ─────────────────────
31
  # The judging sandbox injects API_BASE_URL and API_KEY via their LiteLLM proxy.
32
  # All LLM calls MUST go through these values or the submission will be rejected.
 
112
  flush=True,
113
  )
114
 
115
+ # ── Detector Agent & Smart Blending ───────────────────────────────────────────
116
+
117
+ def detector_agent(prev_info: dict) -> Optional[dict]:
118
+ """Reads the environment's intrinsic physics-based detector output."""
119
+ det = prev_info.get("detector", {})
120
+ if not det or "attack_detected" not in det:
121
+ return None
122
+
123
+ return {
124
+ "attack_detected": det.get("attack_detected", False),
125
+ "attack_type": det.get("attack_type", 0),
126
+ "confidence": det.get("confidence", 0.5),
127
+ "protective_action": det.get("protective_action", 0),
128
+ }
129
+
130
+ def smart_blend_agent(obs: dict, prev_info: dict) -> dict:
131
+ """Uses detector if confident, else falls back to robust heuristic."""
132
+ heur_action = heuristic_agent(obs)
133
+ det_action = detector_agent(prev_info)
134
+
135
+ if not det_action:
136
+ return heur_action
137
+ if det_action["confidence"] < 0.5:
138
+ return heur_action
139
+
140
+ return det_action
141
+
142
+
143
+ # ── Rule-Based Heuristic Agent ────────────────────────────────────────────────
144
 
145
  class HeuristicState:
146
  """Tracks running state for the heuristic agent across steps."""
 
335
  _llm_disabled = False # circuit breaker — flips True after first LLM failure
336
 
337
 
338
+ def llm_agent(obs: dict, prev_info: dict) -> dict:
339
  """Primary agent — calls the LLM through the injected proxy.
340
+ Falls back to smart blending if the API call itself raises an exception.
341
  Uses a circuit breaker: after the first failure, all future calls skip the
342
+ network request and go straight to blending (restoring ~10s runtime).
343
  """
344
  global _llm_disabled
345
  if _llm_disabled:
346
+ return smart_blend_agent(obs, prev_info)
347
 
348
  try:
349
  completion = client.chat.completions.create(
 
358
  )
359
  return parse_llm_response(completion.choices[0].message.content or "")
360
  except Exception as e:
361
+ print(f"[WARN] LLM error ({type(e).__name__}: {e}), disabling LLM for remaining steps", file=sys.stderr, flush=True)
362
  _llm_disabled = True
363
+ return smart_blend_agent(obs, prev_info)
364
 
365
  # ── Episode runner ────────────────────────────────────────────────────────────
366
 
 
396
  # This caps LLM calls at ~150 total across 3 tasks, keeping runtime
397
  # well under the 20-min judging limit even with 3s/call latency.
398
  if step_count % 10 == 0:
399
+ action = llm_agent(obs, info)
400
  else:
401
+ action = smart_blend_agent(obs, info)
402
 
403
  step_resp = _session.post(
404
  f"{ENV_URL}/step",
requirements.txt CHANGED
@@ -5,3 +5,4 @@ numpy==1.26.4
5
  openai>=1.0.0
6
  requests>=2.31.0
7
  openenv-core>=0.2.0
 
 
5
  openai>=1.0.0
6
  requests>=2.31.0
7
  openenv-core>=0.2.0
8
+ python-dotenv>=1.0.0
src/attacks.py CHANGED
@@ -109,7 +109,7 @@ class AttackGenerator:
109
  return 0.0
110
 
111
  def is_active(self, current_step: int) -> bool:
112
- """Checking if the attack is currently active at this step."""
113
  if current_step < self.attack_start_step:
114
  return False
115
 
@@ -123,7 +123,7 @@ class AttackGenerator:
123
 
124
 
125
  def get_attack_type_id(attack_type_str: str) -> int:
126
- """Mapping attack type string to integer ID."""
127
  mapping = {
128
  "none": 0,
129
  "sinusoidal": 1,
 
109
  return 0.0
110
 
111
  def is_active(self, current_step: int) -> bool:
112
+ """Check whether the attack is currently active at this specific step."""
113
  if current_step < self.attack_start_step:
114
  return False
115
 
 
123
 
124
 
125
  def get_attack_type_id(attack_type_str: str) -> int:
126
+ """Map an attack type string to its corresponding integer ID."""
127
  mapping = {
128
  "none": 0,
129
  "sinusoidal": 1,
src/env.py CHANGED
@@ -72,7 +72,7 @@ class PLLAttackEnv:
72
  self.vq_window: deque = deque(maxlen=WINDOW_SIZE)
73
  self.vd_window: deque = deque(maxlen=WINDOW_SIZE)
74
  self.omega_window: deque = deque(maxlen=WINDOW_SIZE)
75
- self.omega_deviation_window: deque = deque(maxlen=WINDOW_SIZE) # Fix 8
76
 
77
  # Detector
78
  self.detector = AdaptiveDetector()
@@ -116,7 +116,7 @@ class PLLAttackEnv:
116
  # Reset history
117
  self.history = []
118
 
119
- # Reset observation windows (Fix 6: no theta_err_window)
120
  self.vq_window = deque(maxlen=WINDOW_SIZE)
121
  self.vd_window = deque(maxlen=WINDOW_SIZE)
122
  self.omega_window = deque(maxlen=WINDOW_SIZE)
@@ -205,12 +205,11 @@ class PLLAttackEnv:
205
  # --- Advance step counter ----------------------------------------
206
  self.step_count += 1
207
 
208
- # --- Episode termination -----------------------------------------
209
- # Fix 4: Task 2 terminates early on lock-loss, not just at MAX_STEPS
210
  if self.step_count >= MAX_STEPS:
211
  self.done = True
212
  elif self.task_id == 2 and self.lock_lost:
213
- self.done = True # early termination — no point continuing
214
 
215
  # --- Physics-informed detector (evaluation/debug only) ------------
216
  detector_output = self.detector.detect(self._get_observation())
@@ -350,7 +349,7 @@ class PLLAttackEnv:
350
  vq_window=list(self.vq_window),
351
  vd_window=list(self.vd_window),
352
  omega_window=list(self.omega_window),
353
- omega_deviation_window=list(self.omega_deviation_window), # Fix 5
354
  raw_voltages=[self.pll.va_m, self.pll.vb_m, self.pll.vc_m],
355
  task_id=self.task_id,
356
  step=self.step_count,
 
72
  self.vq_window: deque = deque(maxlen=WINDOW_SIZE)
73
  self.vd_window: deque = deque(maxlen=WINDOW_SIZE)
74
  self.omega_window: deque = deque(maxlen=WINDOW_SIZE)
75
+ self.omega_deviation_window: deque = deque(maxlen=WINDOW_SIZE)
76
 
77
  # Detector
78
  self.detector = AdaptiveDetector()
 
116
  # Reset history
117
  self.history = []
118
 
119
+ # Reset observation windows
120
  self.vq_window = deque(maxlen=WINDOW_SIZE)
121
  self.vd_window = deque(maxlen=WINDOW_SIZE)
122
  self.omega_window = deque(maxlen=WINDOW_SIZE)
 
205
  # --- Advance step counter ----------------------------------------
206
  self.step_count += 1
207
 
208
+ # Terminate Task 2 early upon losing lock to save computational steps
 
209
  if self.step_count >= MAX_STEPS:
210
  self.done = True
211
  elif self.task_id == 2 and self.lock_lost:
212
+ self.done = True
213
 
214
  # --- Physics-informed detector (evaluation/debug only) ------------
215
  detector_output = self.detector.detect(self._get_observation())
 
349
  vq_window=list(self.vq_window),
350
  vd_window=list(self.vd_window),
351
  omega_window=list(self.omega_window),
352
+ omega_deviation_window=list(self.omega_deviation_window),
353
  raw_voltages=[self.pll.va_m, self.pll.vb_m, self.pll.vc_m],
354
  task_id=self.task_id,
355
  step=self.step_count,
src/graders.py CHANGED
@@ -112,14 +112,14 @@ def grade_task_hard(
112
  attack_active = entry["attack_active"]
113
  attack_detected = entry["attack_detected"]
114
 
115
- # Only counting false alarms before the attack starts
116
  if attack_detected and not attack_active and step < attack_start_step:
117
  false_alarm_count += 1
118
 
119
  if attack_detected and attack_active and first_detection_step is None:
120
  first_detection_step = step
121
 
122
- # Computing base score
123
  if first_detection_step is None:
124
  score = 0.0
125
  elif loss_of_lock_step is not None and first_detection_step < loss_of_lock_step:
@@ -130,7 +130,7 @@ def grade_task_hard(
130
  # No loss of lock occurred but attack was detected
131
  score = 0.3
132
 
133
- # Applying false alarm penalty
134
  penalty = 0.2 * false_alarm_count
135
  score = max(0.01, score - penalty)
136
 
 
112
  attack_active = entry["attack_active"]
113
  attack_detected = entry["attack_detected"]
114
 
115
+ # Only count false alarms before the attack starts
116
  if attack_detected and not attack_active and step < attack_start_step:
117
  false_alarm_count += 1
118
 
119
  if attack_detected and attack_active and first_detection_step is None:
120
  first_detection_step = step
121
 
122
+ # Compute base score
123
  if first_detection_step is None:
124
  score = 0.0
125
  elif loss_of_lock_step is not None and first_detection_step < loss_of_lock_step:
 
130
  # No loss of lock occurred but attack was detected
131
  score = 0.3
132
 
133
+ # Apply false alarm penalty
134
  penalty = 0.2 * false_alarm_count
135
  score = max(0.01, score - penalty)
136