Spaces:
Sleeping
Sleeping
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- .env.example +10 -0
- README.md +81 -73
- inference.py +43 -9
- requirements.txt +1 -0
- src/attacks.py +2 -2
- src/env.py +5 -6
- 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
|
| 22 |
|
| 23 |
-
This OpenEnv environment simulates an SRF-PLL
|
| 24 |
|
| 25 |
## Architecture
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
Grid Voltage (50Hz)
|
| 29 |
│
|
| 30 |
▼
|
| 31 |
-
[FDI Attack Injection]
|
| 32 |
│
|
| 33 |
▼
|
| 34 |
Clarke Transform (αβ)
|
| 35 |
│
|
| 36 |
▼
|
| 37 |
-
Park Transform (dq)
|
| 38 |
│
|
| 39 |
▼
|
| 40 |
-
PI Controller
|
| 41 |
│
|
| 42 |
▼
|
| 43 |
-
Agent
|
| 44 |
│
|
| 45 |
▼
|
| 46 |
-
Agent outputs: attack_detected, attack_type, confidence
|
| 47 |
```
|
| 48 |
|
| 49 |
-
## Inference &
|
| 50 |
|
| 51 |
-
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
2.
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
## Tasks
|
| 59 |
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
|------|----|-----------|-------------|-----------|-------|
|
| 62 |
-
| Sinusoidal FDI
|
| 63 |
-
| Multi-Attack
|
| 64 |
-
| Stealthy
|
| 65 |
|
| 66 |
## Observation Space
|
| 67 |
|
| 68 |
-
|
| 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 |
|
| 79 |
|
| 80 |
-
**Total observation dimension**: 83 (20+
|
| 81 |
|
| 82 |
## Action Space
|
| 83 |
|
| 84 |
-
Agents return a JSON
|
| 85 |
|
| 86 |
| Field | Type | Range | Description |
|
| 87 |
|-------|------|-------|-------------|
|
| 88 |
-
| `attack_detected` | `bool` | — |
|
| 89 |
-
| `attack_type` | `int` | 0–4 | 0=
|
| 90 |
-
| `confidence` | `float` | 0.0–1.0 |
|
| 91 |
-
| `protective_action` | `int` | 0–3 | 0=
|
| 92 |
|
| 93 |
-
## API
|
| 94 |
|
| 95 |
-
|
| 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 |
-
###
|
| 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 |
-
|
| 110 |
```bash
|
| 111 |
-
|
|
|
|
| 112 |
```
|
| 113 |
|
| 114 |
-
|
| 115 |
```bash
|
| 116 |
-
|
|
|
|
| 117 |
```
|
| 118 |
|
| 119 |
-
##
|
| 120 |
|
| 121 |
-
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
```
|
|
|
|
|
|
|
| 127 |
|
| 128 |
-
###
|
| 129 |
|
| 130 |
-
``
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
-
|
| 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
|
| 146 |
|
| 147 |
* **Task 0 (Sinusoidal FDI):** 1.0000
|
| 148 |
-
* **Task 1 (Multi-Attack Classification):** 0.8720
|
| 149 |
-
* **Task 2 (Stealthy Drift):** 0.1639
|
| 150 |
-
* **
|
| 151 |
|
| 152 |
-
|
| 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 deviations—and 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 |
-
# ──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 307 |
Uses a circuit breaker: after the first failure, all future calls skip the
|
| 308 |
-
network request and go straight to
|
| 309 |
"""
|
| 310 |
global _llm_disabled
|
| 311 |
if _llm_disabled:
|
| 312 |
-
return
|
| 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"[
|
| 328 |
_llm_disabled = True
|
| 329 |
-
return
|
| 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 =
|
| 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 |
-
"""
|
| 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 |
-
"""
|
| 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)
|
| 76 |
|
| 77 |
# Detector
|
| 78 |
self.detector = AdaptiveDetector()
|
|
@@ -116,7 +116,7 @@ class PLLAttackEnv:
|
|
| 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,12 +205,11 @@ class PLLAttackEnv:
|
|
| 205 |
# --- Advance step counter ----------------------------------------
|
| 206 |
self.step_count += 1
|
| 207 |
|
| 208 |
-
#
|
| 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
|
| 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),
|
| 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
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
|