OpelSpeedster commited on
Commit
5192692
·
verified ·
1 Parent(s): 01e3e4f

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +10 -0
  2. Dockerfile +18 -0
  3. FirenetCNN1.h5 +3 -0
  4. Meta OpenEnv Hackathon_ Guidelines.md +158 -0
  5. README.md +184 -6
  6. __init__.py +16 -0
  7. client.py +99 -0
  8. debug_env.py +35 -0
  9. environments/wildfire_detection/__init__.py +11 -0
  10. environments/wildfire_detection/firenet_inference.py +187 -0
  11. environments/wildfire_detection/sample_frames/fire_000.jpg +3 -0
  12. environments/wildfire_detection/sample_frames/fire_001.jpg +3 -0
  13. environments/wildfire_detection/sample_frames/fire_002.jpg +3 -0
  14. environments/wildfire_detection/sample_frames/fire_003.jpg +3 -0
  15. environments/wildfire_detection/sample_frames/fire_004.jpg +3 -0
  16. environments/wildfire_detection/sample_frames/fire_005.jpg +3 -0
  17. environments/wildfire_detection/sample_frames/fire_015.png +0 -0
  18. environments/wildfire_detection/sample_frames/no_fire_008.jpg +3 -0
  19. environments/wildfire_detection/sample_frames/no_fire_009.jpg +3 -0
  20. environments/wildfire_detection/sample_frames/no_fire_013.jpg +0 -0
  21. environments/wildfire_detection/sample_frames/no_fire_014.jpg +0 -0
  22. environments/wildfire_detection/sample_frames/smoke_006.jpg +3 -0
  23. environments/wildfire_detection/sample_frames/smoke_007.jpg +3 -0
  24. environments/wildfire_detection/sample_frames/smoke_010.jpg +0 -0
  25. environments/wildfire_detection/sample_frames/smoke_011.jpg +0 -0
  26. environments/wildfire_detection/sample_frames/smoke_012.jpg +0 -0
  27. environments/wildfire_detection/wildfire_env.py +393 -0
  28. inference.py +261 -0
  29. inference_rl.py +120 -0
  30. models.py +42 -0
  31. openenv.yaml +6 -0
  32. openenv_wildfire_detection.egg-info/PKG-INFO +14 -0
  33. openenv_wildfire_detection.egg-info/SOURCES.txt +30 -0
  34. openenv_wildfire_detection.egg-info/dependency_links.txt +1 -0
  35. openenv_wildfire_detection.egg-info/entry_points.txt +2 -0
  36. openenv_wildfire_detection.egg-info/requires.txt +10 -0
  37. openenv_wildfire_detection.egg-info/top_level.txt +1 -0
  38. push_hf.py +21 -0
  39. pyproject.toml +40 -0
  40. q_model.pkl +3 -0
  41. requirements.txt +107 -0
  42. run_server.py +7 -0
  43. server/__init__.py +11 -0
  44. server/app.py +64 -0
  45. server/multipen_environment.py +104 -0
  46. server/requirements.txt +9 -0
  47. server/wildfire_environment.py +102 -0
  48. test_env.py +19 -0
  49. train_rl.py +209 -0
  50. uv.lock +0 -0
.gitattributes CHANGED
@@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ environments/wildfire_detection/sample_frames/fire_000.jpg filter=lfs diff=lfs merge=lfs -text
37
+ environments/wildfire_detection/sample_frames/fire_001.jpg filter=lfs diff=lfs merge=lfs -text
38
+ environments/wildfire_detection/sample_frames/fire_002.jpg filter=lfs diff=lfs merge=lfs -text
39
+ environments/wildfire_detection/sample_frames/fire_003.jpg filter=lfs diff=lfs merge=lfs -text
40
+ environments/wildfire_detection/sample_frames/fire_004.jpg filter=lfs diff=lfs merge=lfs -text
41
+ environments/wildfire_detection/sample_frames/fire_005.jpg filter=lfs diff=lfs merge=lfs -text
42
+ environments/wildfire_detection/sample_frames/no_fire_008.jpg filter=lfs diff=lfs merge=lfs -text
43
+ environments/wildfire_detection/sample_frames/no_fire_009.jpg filter=lfs diff=lfs merge=lfs -text
44
+ environments/wildfire_detection/sample_frames/smoke_006.jpg filter=lfs diff=lfs merge=lfs -text
45
+ environments/wildfire_detection/sample_frames/smoke_007.jpg filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Wildfire Detection OpenEnv Environment
2
+
3
+ ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
4
+ FROM ${BASE_IMAGE}
5
+
6
+ WORKDIR /app
7
+
8
+ COPY . /app/env
9
+
10
+ WORKDIR /app/env
11
+
12
+ # Install dependencies directly
13
+ RUN pip install --no-cache-dir -e .
14
+
15
+ EXPOSE 8000
16
+
17
+ ENV ENABLE_WEB_INTERFACE=true
18
+ CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]
FirenetCNN1.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d3bc11fdafb36cf0e43ea1af44015097a62a67118b43d27f0badd2b5867c8525
3
+ size 25352336
Meta OpenEnv Hackathon_ Guidelines.md ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## **🚀 Hackathon Submission Guidelines (OpenEnv RL Challenge)**
2
+
3
+ ### **1\. Project Structure**
4
+
5
+ * Your inference script **must** be named inference.py
6
+ * It **must be located in the root directory** of your project
7
+
8
+ ---
9
+
10
+ ### **2\. LLM Usage Requirements**
11
+
12
+ * You **must use the OpenAI Client** for all LLM calls
13
+ * Do **not** use alternative SDKs or direct HTTP calls
14
+
15
+ ---
16
+
17
+ ### **3\. Required Environment Variables**
18
+
19
+ Your inference.py must read the following environment variables:
20
+
21
+ * API\_BASE\_URL
22
+ * Description: API endpoint for the LLM
23
+ * Requirement: **Must include a default value**
24
+ * MODEL\_NAME
25
+ * Description: Model identifier used for inference
26
+ * Requirement: **Must include a default value**
27
+ * HF\_TOKEN
28
+ * Description: Hugging Face API token
29
+ * Requirement: **Mandatory (no default required)**
30
+
31
+ **4\. INFERENCE OUTPUT FORMAT**
32
+
33
+ The script must emit exactly three line types to stdout, in this order:
34
+ \[START\] task=\<task\_name\> env=\<benchmark\> model=\<model\_name\>
35
+ \[STEP\] step=\<n\> action=\<action\_str\> reward=\<0.00\> done=\<true|false\> error=\<msg|null\>
36
+ \[END\] success=\<true|false\> steps=\<n\> rewards=\<r1,r2,...,rn\>
37
+
38
+ Rules:
39
+ \- One \[START\] line at episode begin.
40
+ \- One \[STEP\] line per step, immediately after env.step() returns.
41
+ \- One \[END\] line after env.close(), always emitted (even on exception).
42
+ \- reward and rewards are formatted to 2 decimal places.
43
+ \- done and success are lowercase booleans: true or false.
44
+ \- error is the raw last\_action\_error string, or null if none.
45
+ \- All fields on a single line with no newlines within a line.
46
+ Example:
47
+ \[START\] task=click-test env=miniwob model=Qwen3-VL-30B
48
+ \[STEP\] step=1 action=click('123') reward=0.00 done=false error=null
49
+ \[STEP\] step=2 action=fill('456','text') reward=0.00 done=false error=null
50
+ \[STEP\] step=3 action=click('789') reward=1.00 done=true error=null
51
+ \[END\] success=true steps=3 rewards=0.00,0.00,1.00
52
+
53
+ #### **✅ Example (inference.py)**
54
+
55
+ ```py
56
+ import os
57
+ from openai import OpenAI
58
+
59
+ # Read environment variables with defaults where required
60
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://api.openai.com/v1")
61
+ MODEL_NAME = os.getenv("MODEL_NAME", "gpt-4.1-mini")
62
+ HF_TOKEN = os.getenv("HF_TOKEN")
63
+
64
+ if HF_TOKEN is None:
65
+ raise ValueError("HF_TOKEN environment variable is required")
66
+
67
+ # Initialize OpenAI client
68
+ client = OpenAI(
69
+ base_url=API_BASE_URL,
70
+ api_key=HF_TOKEN
71
+ )
72
+
73
+ def run_inference(prompt: str):
74
+ response = client.chat.completions.create(
75
+ model=MODEL_NAME,
76
+ messages=[
77
+ {"role": "user", "content": prompt}
78
+ ]
79
+ )
80
+ response = response.choices[0].message.content
81
+ # Print output based on above given format
82
+
83
+
84
+ if __name__ == "__main__":
85
+ print(run_inference("Hello from OpenEnv!"))
86
+ ```
87
+
88
+ ---
89
+
90
+ ### **4\. Hugging Face Space Guidelines**
91
+
92
+ * Building a Hugging Face Space can take significant time, especially if multiple spaces are active
93
+ * To avoid delays:
94
+ * Turn off all unnecessary spaces
95
+ * Keep only your primary submission space running
96
+
97
+ ---
98
+
99
+ ### **5\. Submission Validation Rules**
100
+
101
+ * The system will **check if your Hugging Face Space is live**
102
+ * If your space is **not in a running state**, your submission will **fail automatically**
103
+
104
+ Before submitting:
105
+
106
+ * Ensure your space is fully built
107
+ * Confirm it is in the **“Running”** state
108
+
109
+ ---
110
+
111
+ ### **6\. Hardware Requirements**
112
+
113
+ * Your solution will be executed inside a Docker container with limited resources
114
+ * It **must run within the following constraints**:
115
+ * **2 vCPU**
116
+ * **8 GB RAM**
117
+
118
+ 👉 Ensure your model, dependencies, and runtime fit within these limits. Submissions exceeding these constraints may fail during evaluation.
119
+
120
+ ---
121
+
122
+ ### **6\. Resubmissions**
123
+
124
+ * You are allowed to **resubmit your project multiple times**
125
+ * If your submission fails validation, you can:
126
+ * Fix the issues
127
+ * Ensure your Hugging Face Space is running
128
+ * Submit again
129
+
130
+ 👉 There is **no penalty for resubmitting**, so iterate until your submission passes all checks.
131
+
132
+ ---
133
+
134
+ ### **⚠️ Common Failure Cases (Avoid These)**
135
+
136
+ * inference.py not in root directory
137
+ * Missing default values for API\_BASE\_URL or MODEL\_NAME
138
+ * Missing HF\_TOKEN
139
+ * Hugging Face Space still building during submission
140
+ * Space stopped due to multiple active deployments
141
+
142
+ ### **🚀 Reference projects to guide you**
143
+
144
+ Here are some strong examples from the San Francisco edition to help you understand how to structure your environment:
145
+
146
+ * Calendar Environment Server
147
+ [https://github.com/meta-pytorch/OpenEnv/tree/main/envs/calendar\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/calendar_env)
148
+ * Reasoning Gym Environment Server
149
+ [https://github.com/meta-pytorch/OpenEnv/tree/main/envs/reasoning\_gym\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/reasoning_gym_env)
150
+ * TB2 Environment Server
151
+ [https://github.com/meta-pytorch/OpenEnv/tree/main/envs/tbench2\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/tbench2_env)
152
+ * CARLA Environment Server
153
+ [https://github.com/meta-pytorch/OpenEnv/tree/main/envs/carla\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/carla_env)
154
+ * REPL Environment Server
155
+ [https://github.com/meta-pytorch/OpenEnv/tree/main/envs/repl\_env](https://github.com/meta-pytorch/OpenEnv/tree/main/envs/repl_env)
156
+
157
+ Use these as direction, not as templates. Focus on understanding structure and approach.
158
+
README.md CHANGED
@@ -1,11 +1,189 @@
1
  ---
2
- title: Multipen RL OpenEnv
3
- emoji: 🐠
4
- colorFrom: green
5
- colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
- license: apache-2.0
 
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Wildfire Detection Environment
3
+ emoji: 🔥
4
+ colorFrom: red
5
+ colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
+ app_port: 8000
9
+ base_path: /web
10
+ tags:
11
+ - openenv
12
+ - wildfire-detection
13
+ - reinforcement-learning
14
+ - firenet-cnn
15
  ---
16
 
17
+ # Wildfire Detection RL Environment
18
+
19
+ An OpenEnv-compatible reinforcement learning environment for autonomous wildfire patrol using the FirenetCNN model. The agent receives sequential camera observations, runs inference, and decides actions to detect fire or smoke as early as possible.
20
+
21
+ ## Features
22
+
23
+ - **Exact FirenetCNN Inference** - Uses the original model from the Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques repository
24
+ - **Grad-CAM XAI** - Explainable AI with heatmap visualization for model decisions
25
+ - **Sequential Decision Making** - Agent processes multiple frames and learns from context
26
+ - **Reward-based Learning** - Optimizes for early detection while minimizing false alarms
27
+
28
+ ## Quick Start
29
+
30
+ ### Using the Docker Image
31
+
32
+ ```bash
33
+ # Pull and run the pre-built image
34
+ docker run -p 8000:8000 wildfire-detection
35
+ ```
36
+
37
+ ### Building from Source
38
+
39
+ ```bash
40
+ # Build the Docker image
41
+ docker build -t wildfire-detection .
42
+
43
+ # Run the container
44
+ docker run -p 8000:8000 wildfire-detection
45
+ ```
46
+
47
+ ### Local Development
48
+
49
+ ```bash
50
+ # Install dependencies
51
+ pip install -e .
52
+
53
+ # Run the server
54
+ python run_server.py
55
+ ```
56
+
57
+ ## API Usage
58
+
59
+ ### Reset Environment
60
+
61
+ ```bash
62
+ curl -X POST http://localhost:8000/reset
63
+ ```
64
+
65
+ ### Take Action
66
+
67
+ ```bash
68
+ curl -X POST http://localhost:8000/step \
69
+ -H "Content-Type: application/json" \
70
+ -d '{"action": "Alert"}'
71
+ ```
72
+
73
+ ### Get Schema
74
+
75
+ ```bash
76
+ curl http://localhost:8000/schema
77
+ ```
78
+
79
+ Access the interactive API docs at http://localhost:8000/docs
80
+
81
+ ## Inference Script
82
+
83
+ The `inference.py` script runs an LLM agent that makes decisions:
84
+
85
+ ```bash
86
+ # Set API key (injected by judges at evaluation)
87
+ export API_KEY=your_huggingface_token
88
+
89
+ # Run inference
90
+ python inference.py
91
+ ```
92
+
93
+ ### Environment Variables
94
+
95
+ | Variable | Default | Required |
96
+ |----------|---------|----------|
97
+ | `API_BASE_URL` | `https://router.huggingface.co/v1` | No |
98
+ | `MODEL_NAME` | `Qwen/Qwen2.5-3B-Instruct` | No |
99
+ | `API_KEY` | - | Yes |
100
+
101
+ ### Output Format
102
+
103
+ The script follows the hackathon output format:
104
+
105
+ ```
106
+ [START] task=wildfire_detection env=wildfire_detection model=Qwen/Qwen2.5-3B-Instruct
107
+ [STEP] step=1 action=Alert reward=-1.00 done=false error=null
108
+ [STEP] step=2 action=Scan reward=0.50 done=false error=null
109
+ [END] success=true steps=16 rewards=-1.00,0.50,2.00,...
110
+ ```
111
+
112
+ ## Actions
113
+
114
+ | Action | Description |
115
+ |--------|-------------|
116
+ | `Alert` | Raise immediate alert if fire/smoke detected |
117
+ | `Scan` | Continue scanning/observing the current frame |
118
+ | `Ignore` | No action needed, clear conditions |
119
+ | `Deploy` | Deploy resources to the detected location |
120
+
121
+ ## Reward Structure
122
+
123
+ | Reward | Condition |
124
+ |--------|-----------|
125
+ | +2.00 | Correct detection with confidence ≥ 0.85 and clear Grad-CAM alignment |
126
+ | +1.00 | Correct class but lower confidence |
127
+ | +0.50 | Partial credit (right class, weak XAI) |
128
+ | -0.75 | False positive (Alert/Deploy on no_fire) |
129
+ | -1.00 | False negative (missed fire/smoke) |
130
+ | -0.50 | Inaction ("Ignore") when fire/smoke is present |
131
+ | -2.00 | Terminal failure (too many missed detections) |
132
+
133
+ ## Grading
134
+
135
+ The grader evaluates each episode:
136
+ - **Success**: All fire/smoke frames correctly alerted with zero false positives
137
+ - **Score**: Normalized 0.0-1.0 based on cumulative reward
138
+
139
+ ## Project Structure
140
+
141
+ ```
142
+ multipen/
143
+ ├── inference.py # LLM agent inference script
144
+ ├── wildfire_env.py # RL environment (Gym-compatible)
145
+ ├── firenet_inference.py # FirenetCNN + Grad-CAM
146
+ ├── models.py # Action/Observation models
147
+ ├── openenv.yaml # OpenEnv configuration
148
+ ├── pyproject.toml # Dependencies
149
+ ├── Dockerfile # Container definition
150
+ ├── server/
151
+ │ ├── app.py # FastAPI server
152
+ │ └── wildfire_environment.py # OpenEnv wrapper
153
+ └── environments/
154
+ └── wildfire_detection/
155
+ ├── wildfire_env.py # Environment implementation
156
+ ├── firenet_inference.py # Model inference
157
+ └── sample_frames/ # Test images (16 frames)
158
+ ```
159
+
160
+ ## Deployment to Hugging Face Spaces
161
+
162
+ ```bash
163
+ # Push to Hugging Face
164
+ openenv push
165
+
166
+ # Or with options
167
+ openenv push --namespace my-org --private
168
+ ```
169
+
170
+ ## Testing
171
+
172
+ ```bash
173
+ # Test environment directly
174
+ python test_env.py
175
+
176
+ # Test API
177
+ curl http://localhost:8000/health
178
+ curl http://localhost:8000/schema
179
+ ```
180
+
181
+ ## Model
182
+
183
+ Uses FirenetCNN (MobileNetV2-based) from:
184
+ https://github.com/OpelSpeedster/Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques
185
+
186
+ The model classifies images into:
187
+ - `no_fire` - No fire detected
188
+ - `fire` - Fire detected
189
+ - `smoke` - Smoke detected
__init__.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Multipen Environment."""
8
+
9
+ from .client import MultipenEnv
10
+ from .models import MultipenAction, MultipenObservation
11
+
12
+ __all__ = [
13
+ "MultipenAction",
14
+ "MultipenObservation",
15
+ "MultipenEnv",
16
+ ]
client.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Multipen Environment Client."""
8
+
9
+ from typing import Dict
10
+
11
+ from openenv.core import EnvClient
12
+ from openenv.core.client_types import StepResult
13
+ from openenv.core.env_server.types import State
14
+
15
+ from .models import MultipenAction, MultipenObservation
16
+
17
+
18
+ class MultipenEnv(
19
+ EnvClient[MultipenAction, MultipenObservation, State]
20
+ ):
21
+ """
22
+ Client for the Multipen Environment.
23
+
24
+ This client maintains a persistent WebSocket connection to the environment server,
25
+ enabling efficient multi-step interactions with lower latency.
26
+ Each client instance has its own dedicated environment session on the server.
27
+
28
+ Example:
29
+ >>> # Connect to a running server
30
+ >>> with MultipenEnv(base_url="http://localhost:8000") as client:
31
+ ... result = client.reset()
32
+ ... print(result.observation.echoed_message)
33
+ ...
34
+ ... result = client.step(MultipenAction(message="Hello!"))
35
+ ... print(result.observation.echoed_message)
36
+
37
+ Example with Docker:
38
+ >>> # Automatically start container and connect
39
+ >>> client = MultipenEnv.from_docker_image("multipen-env:latest")
40
+ >>> try:
41
+ ... result = client.reset()
42
+ ... result = client.step(MultipenAction(message="Test"))
43
+ ... finally:
44
+ ... client.close()
45
+ """
46
+
47
+ def _step_payload(self, action: MultipenAction) -> Dict:
48
+ """
49
+ Convert MultipenAction to JSON payload for step message.
50
+
51
+ Args:
52
+ action: MultipenAction instance
53
+
54
+ Returns:
55
+ Dictionary representation suitable for JSON encoding
56
+ """
57
+ return {
58
+ "message": action.message,
59
+ }
60
+
61
+ def _parse_result(self, payload: Dict) -> StepResult[MultipenObservation]:
62
+ """
63
+ Parse server response into StepResult[MultipenObservation].
64
+
65
+ Args:
66
+ payload: JSON response data from server
67
+
68
+ Returns:
69
+ StepResult with MultipenObservation
70
+ """
71
+ obs_data = payload.get("observation", {})
72
+ observation = MultipenObservation(
73
+ echoed_message=obs_data.get("echoed_message", ""),
74
+ message_length=obs_data.get("message_length", 0),
75
+ done=payload.get("done", False),
76
+ reward=payload.get("reward"),
77
+ metadata=obs_data.get("metadata", {}),
78
+ )
79
+
80
+ return StepResult(
81
+ observation=observation,
82
+ reward=payload.get("reward"),
83
+ done=payload.get("done", False),
84
+ )
85
+
86
+ def _parse_state(self, payload: Dict) -> State:
87
+ """
88
+ Parse server response into State object.
89
+
90
+ Args:
91
+ payload: JSON response from state request
92
+
93
+ Returns:
94
+ State object with episode_id and step_count
95
+ """
96
+ return State(
97
+ episode_id=payload.get("episode_id"),
98
+ step_count=payload.get("step_count", 0),
99
+ )
debug_env.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ import numpy as np
4
+
5
+ sys.path.insert(0, r"C:\Users\ASUS\OneDrive\Desktop\RL\multipen")
6
+
7
+ from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
8
+
9
+ env = WildfireDetectionEnv()
10
+ actions = ["Alert", "Scan", "Ignore", "Deploy"]
11
+
12
+ print("=== Analyzing Environment ===")
13
+
14
+ for episode in range(2):
15
+ print(f"\n--- Episode {episode + 1} ---")
16
+ obs = env.reset()
17
+
18
+ for step in range(3):
19
+ gt = obs["ground_truth"]
20
+ if hasattr(gt, "__len__") and gt.ndim > 0:
21
+ gt_idx = int(gt[0])
22
+ else:
23
+ gt_idx = int(gt)
24
+ gt_label = ["fire", "smoke", "no_fire"][gt_idx]
25
+
26
+ print(f"Step {step + 1}: GT={gt_label}, Pred={obs['prediction']}")
27
+
28
+ for a_idx, a_name in enumerate(actions):
29
+ obs2, r, done, info = env.step(a_idx)
30
+ print(f" {a_name}: reward={r:.2f}")
31
+
32
+ if step < 2:
33
+ obs = obs2
34
+
35
+ env.close()
environments/wildfire_detection/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Wildfire Detection OpenEnv Environment
2
+
3
+ This package provides a minimal OpenEnv-compatible RL environment
4
+ that follows the WildfireDetection skill specification. It is designed
5
+ to be extended with the exact FirenetCNN inference pipeline from the
6
+ opposed GitHub repo without modifying that repository.
7
+ """
8
+
9
+ from .wildfire_env import WildfireDetectionEnv
10
+
11
+ __all__ = ["WildfireDetectionEnv"]
environments/wildfire_detection/firenet_inference.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FirenetCNN Inference Module
3
+
4
+ This module provides the exact inference and Grad-CAM implementation
5
+ from the Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques repository.
6
+ No modifications to the original repo code.
7
+ """
8
+
9
+ import os
10
+ import cv2
11
+ import numpy as np
12
+ import tensorflow as tf
13
+ from tensorflow.keras.models import load_model, Model
14
+ from typing import Tuple, Dict, Any, Optional
15
+
16
+
17
+ MODEL_PATH = os.getenv("FIRENET_MODEL_PATH", "FirenetCNN1.h5")
18
+ TARGET_SIZE = (224, 224)
19
+ CLASS_LABELS = ["no_fire", "fire", "smoke"]
20
+ LAST_CONV_LAYER_NAME = "out_relu"
21
+
22
+
23
+ def make_gradcam_heatmap(img_array, model, last_conv_layer_name, pred_index):
24
+ """Creates a Grad-CAM heatmap."""
25
+ grad_model = Model(
26
+ [model.inputs], [model.get_layer(last_conv_layer_name).output, model.output]
27
+ )
28
+
29
+ with tf.GradientTape() as tape:
30
+ last_conv_layer_output, preds = grad_model(img_array)
31
+ class_channel = preds[:, pred_index]
32
+
33
+ grads = tape.gradient(class_channel, last_conv_layer_output)
34
+ pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
35
+
36
+ last_conv_layer_output = last_conv_layer_output[0]
37
+ heatmap = last_conv_layer_output @ pooled_grads[..., tf.newaxis]
38
+ heatmap = tf.squeeze(heatmap)
39
+
40
+ heatmap = tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap)
41
+ return heatmap.numpy()
42
+
43
+
44
+ def overlay_heatmap(original_img, heatmap, alpha=0.4):
45
+ """Applies the heatmap to the original image."""
46
+ heatmap = cv2.resize(heatmap, (original_img.shape[1], original_img.shape[0]))
47
+ heatmap = np.uint8(255 * heatmap)
48
+ heatmap = cv2.applyColorMap(heatmap, cv2.COLORMAP_JET)
49
+ superimposed_img = heatmap * alpha + original_img
50
+ superimposed_img = np.clip(superimposed_img, 0, 255).astype(np.uint8)
51
+ return superimposed_img
52
+
53
+
54
+ class FirenetInference:
55
+ """Wrapper class for FirenetCNN inference with Grad-CAM."""
56
+
57
+ def __init__(self, model_path: Optional[str] = None):
58
+ self.model_path = model_path or MODEL_PATH
59
+ self.model = None
60
+ self._load_model()
61
+
62
+ def _load_model(self):
63
+ """Load the FirenetCNN model."""
64
+ if os.path.exists(self.model_path):
65
+ self.model = load_model(self.model_path, compile=False)
66
+ else:
67
+ raise FileNotFoundError(f"Model not found: {self.model_path}")
68
+
69
+ def predict(self, image: np.ndarray) -> Dict[str, Any]:
70
+ """
71
+ Run inference on an image and return prediction + Grad-CAM info.
72
+
73
+ Args:
74
+ image: BGR image (from cv2.imread) or RGB image
75
+
76
+ Returns:
77
+ dict with keys:
78
+ - label: 'fire', 'smoke', or 'no_fire'
79
+ - confidence: float (0.0-1.0)
80
+ - class_idx: int (0=no_fire, 1=fire, 2=smoke)
81
+ - heatmap: numpy array or None
82
+ - gradcam_summary: str describing heatmap location
83
+ """
84
+ original_image = image.copy()
85
+
86
+ if image.shape[:2] != TARGET_SIZE[::-1]:
87
+ image = cv2.resize(image, TARGET_SIZE)
88
+
89
+ input_image_scaled = image.astype(np.float32) / 255.0
90
+ input_image_batch = np.expand_dims(input_image_scaled, axis=0)
91
+
92
+ preds = self.model.predict(input_image_batch, verbose=0)
93
+ confidence = float(np.max(preds[0]))
94
+ class_idx = int(np.argmax(preds[0]))
95
+ label = CLASS_LABELS[class_idx]
96
+
97
+ heatmap = None
98
+ gradcam_summary = "N/A"
99
+
100
+ if label != "no_fire":
101
+ heatmap = make_gradcam_heatmap(
102
+ input_image_batch, self.model, LAST_CONV_LAYER_NAME, class_idx
103
+ )
104
+ gradcam_summary = self._summarize_heatmap(heatmap)
105
+
106
+ return {
107
+ "label": label,
108
+ "confidence": confidence,
109
+ "class_idx": class_idx,
110
+ "heatmap": heatmap,
111
+ "gradcam_summary": gradcam_summary,
112
+ }
113
+
114
+ def _summarize_heatmap(self, heatmap: np.ndarray) -> str:
115
+ """
116
+ Summarize the heatmap location as center/left/right/top/bottom.
117
+ """
118
+ h, w = heatmap.shape
119
+ threshold = 0.5
120
+
121
+ heatmap_thresh = (heatmap > threshold).astype(np.float32)
122
+
123
+ h_half = h // 2
124
+ w_half = w // 2
125
+
126
+ top_region = heatmap_thresh[:h_half, :].sum()
127
+ bottom_region = heatmap_thresh[h_half:, :].sum()
128
+ left_region = heatmap_thresh[:, :w_half].sum()
129
+ right_region = heatmap_thresh[:, w_half:].sum()
130
+ center_region = heatmap_thresh[h // 4 : 3 * h // 4, w // 4 : 3 * w // 4].sum()
131
+
132
+ total = heatmap_thresh.sum()
133
+ if total == 0:
134
+ return "no significant hotspot"
135
+
136
+ regions = {
137
+ "center": center_region,
138
+ "top": top_region,
139
+ "bottom": bottom_region,
140
+ "left": left_region,
141
+ "right": right_region,
142
+ }
143
+
144
+ dominant = max(regions, key=regions.get)
145
+ return f"hotspot at {dominant}"
146
+
147
+
148
+ def load_image_for_inference(image_path: str) -> Optional[np.ndarray]:
149
+ """Load and preprocess an image for inference."""
150
+ if not os.path.exists(image_path):
151
+ return None
152
+ image = cv2.imread(image_path)
153
+ return image
154
+
155
+
156
+ def extract_video_frames(video_path: str, frame_interval: int = 10) -> list:
157
+ """
158
+ Extract frames from a video at regular intervals.
159
+
160
+ Args:
161
+ video_path: Path to video file
162
+ frame_interval: Extract every N frames
163
+
164
+ Returns:
165
+ List of frames as numpy arrays
166
+ """
167
+ frames = []
168
+ if not os.path.exists(video_path):
169
+ return frames
170
+
171
+ cap = cv2.VideoCapture(video_path)
172
+ if not cap.isOpened():
173
+ return frames
174
+
175
+ frame_count = 0
176
+ while cap.isOpened():
177
+ ret, frame = cap.read()
178
+ if not ret:
179
+ break
180
+
181
+ if frame_count % frame_interval == 0:
182
+ frames.append(frame)
183
+
184
+ frame_count += 1
185
+
186
+ cap.release()
187
+ return frames
environments/wildfire_detection/sample_frames/fire_000.jpg ADDED

Git LFS Details

  • SHA256: 714c264acd4b095d6cbb0be30c25704d1e2547476a3472a734d72eb877045b1f
  • Pointer size: 132 Bytes
  • Size of remote file: 1.17 MB
environments/wildfire_detection/sample_frames/fire_001.jpg ADDED

Git LFS Details

  • SHA256: c954332a91b6d9d60ffdd0f7b04646183d89f484a8da261a0acf339ff23041e8
  • Pointer size: 132 Bytes
  • Size of remote file: 1.33 MB
environments/wildfire_detection/sample_frames/fire_002.jpg ADDED

Git LFS Details

  • SHA256: fdb7bc6241a92ad45a5e2521ce5af0dde09c5a65828dbc87a9d6ad97627e2e98
  • Pointer size: 132 Bytes
  • Size of remote file: 1.31 MB
environments/wildfire_detection/sample_frames/fire_003.jpg ADDED

Git LFS Details

  • SHA256: c37ae3e2e6ef60a3e8540df2d63527acce1ec2c685b6886c1b23f8950acd5c9a
  • Pointer size: 132 Bytes
  • Size of remote file: 1.31 MB
environments/wildfire_detection/sample_frames/fire_004.jpg ADDED

Git LFS Details

  • SHA256: 056261f3807ee8de111e75d560dc1bc6effb49d7ac7ccf337641c94ef87c4744
  • Pointer size: 132 Bytes
  • Size of remote file: 1.27 MB
environments/wildfire_detection/sample_frames/fire_005.jpg ADDED

Git LFS Details

  • SHA256: 7017834585b280fa67e6fcf38012546546c8f6b1382bbfbefc780d97c75ebeb5
  • Pointer size: 132 Bytes
  • Size of remote file: 1.31 MB
environments/wildfire_detection/sample_frames/fire_015.png ADDED
environments/wildfire_detection/sample_frames/no_fire_008.jpg ADDED

Git LFS Details

  • SHA256: a9efc6224ffc3e1cdcb48e37047992cbab904ed13af6641b63a7e9c4db146454
  • Pointer size: 132 Bytes
  • Size of remote file: 1.22 MB
environments/wildfire_detection/sample_frames/no_fire_009.jpg ADDED

Git LFS Details

  • SHA256: 968e661ffa2bf0f3e1c65ec6e557d6ad970c0859418b2b18871687067ee34b31
  • Pointer size: 132 Bytes
  • Size of remote file: 1.22 MB
environments/wildfire_detection/sample_frames/no_fire_013.jpg ADDED
environments/wildfire_detection/sample_frames/no_fire_014.jpg ADDED
environments/wildfire_detection/sample_frames/smoke_006.jpg ADDED

Git LFS Details

  • SHA256: 840bb5b3625873b9757bb6fcb8b200d2fb38bcaa5238085b43c4d7448a666f49
  • Pointer size: 132 Bytes
  • Size of remote file: 1.26 MB
environments/wildfire_detection/sample_frames/smoke_007.jpg ADDED

Git LFS Details

  • SHA256: eacfe754bf7142bf311fd0e9f4c8e761cd614fd8ce87c9cc008516ebc3818157
  • Pointer size: 132 Bytes
  • Size of remote file: 1.22 MB
environments/wildfire_detection/sample_frames/smoke_010.jpg ADDED
environments/wildfire_detection/sample_frames/smoke_011.jpg ADDED
environments/wildfire_detection/sample_frames/smoke_012.jpg ADDED
environments/wildfire_detection/wildfire_env.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """WildfireDetectionEnv
2
+ OpenEnv-compatible environment for autonomous wildfire patrol.
3
+
4
+ Notes:
5
+ - Action space: ["Alert", "Scan", "Ignore", "Deploy"]
6
+ - Uses exact FirenetCNN inference from the repo (no modifications)
7
+ - Implements reward structure from skill description
8
+ - Includes grader for episode evaluation
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import json
15
+ from typing import Dict, Any, Optional, Tuple, List
16
+ import numpy as np
17
+ from PIL import Image
18
+
19
+ try:
20
+ import gym
21
+ from gym import spaces
22
+ except Exception:
23
+ gym = None
24
+ spaces = None
25
+
26
+ class DummySpace:
27
+ def __init__(self, *args, **kwargs):
28
+ pass
29
+
30
+ def sample(self):
31
+ return None
32
+
33
+ class DummyEnv:
34
+ pass
35
+
36
+ spaces = type(
37
+ "spaces", (), {"Dict": DummySpace, "Box": DummySpace, "Discrete": DummySpace}
38
+ )
39
+ gym = type("gym", (), {"Env": DummyEnv})
40
+
41
+ try:
42
+ from .firenet_inference import FirenetInference, load_image_for_inference
43
+ except (ModuleNotFoundError, ImportError):
44
+ FirenetInference = None
45
+ load_image_for_inference = None
46
+
47
+
48
+ class WildfireDetectionEnv(
49
+ gym.Env if "gym" in globals() and gym is not None else object
50
+ ):
51
+ """OpenEnv environment for wildfire detection using FirenetCNN."""
52
+
53
+ ACTIONS = ["Alert", "Scan", "Ignore", "Deploy"]
54
+ CLASS_LABELS = ["no_fire", "fire", "smoke"]
55
+ MAX_MISSES = 3
56
+ LOG_FILE = "wildfire_episode_log.txt"
57
+
58
+ def __init__(
59
+ self,
60
+ frames_dir: Optional[str] = None,
61
+ model_path: Optional[str] = None,
62
+ frames_labels: Optional[Dict[str, str]] = None,
63
+ ):
64
+ super().__init__()
65
+
66
+ self.frames_dir = frames_dir or os.path.join(
67
+ os.path.dirname(__file__), "sample_frames"
68
+ )
69
+ self.model_path = model_path or os.path.join(
70
+ os.path.dirname(__file__),
71
+ "..",
72
+ "..",
73
+ "FirenetCNN1.h5",
74
+ )
75
+
76
+ self._frames = self._discover_frames()
77
+ self._max_frames = len(self._frames)
78
+ self._frame_idx = 0
79
+ self._step = 0
80
+ self._missed_count = 0
81
+ self._cumulative_reward = 0.0
82
+ self._episode_history: List[Dict[str, Any]] = []
83
+
84
+ self._inference = None
85
+ self._load_inference()
86
+
87
+ self._ground_truth_labels = frames_labels or self._load_labels()
88
+
89
+ self._frame_height = 224
90
+ self._frame_width = 224
91
+ self.observation_space = spaces.Dict(
92
+ {
93
+ "image": spaces.Box(
94
+ low=0,
95
+ high=255,
96
+ shape=(self._frame_height, self._frame_width, 3),
97
+ dtype=np.uint8,
98
+ ),
99
+ "prediction": spaces.Box(
100
+ low=0.0, high=1.0, shape=(3,), dtype=np.float32
101
+ ),
102
+ "gradcam": spaces.Box(low=0.0, high=1.0, shape=(1,), dtype=np.float32),
103
+ "gradcam_summary": spaces.Box(
104
+ low=0, high=1, shape=(1,), dtype=np.int32
105
+ ),
106
+ "frame_id": spaces.Box(
107
+ low=0, high=max(0, self._max_frames - 1), shape=(1,), dtype=np.int32
108
+ ),
109
+ "step": spaces.Box(low=0, high=1_000_000, shape=(1,), dtype=np.int32),
110
+ "ground_truth": spaces.Box(low=0, high=2, shape=(1,), dtype=np.int32),
111
+ }
112
+ )
113
+
114
+ self.action_space = spaces.Discrete(len(self.ACTIONS))
115
+ self._action_str = lambda idx: self.ACTIONS[int(idx)]
116
+
117
+ def _load_inference(self):
118
+ if os.path.exists(self.model_path):
119
+ try:
120
+ self._inference = FirenetInference(self.model_path)
121
+ except Exception:
122
+ self._inference = None
123
+ else:
124
+ self._inference = None
125
+
126
+ def _load_labels(self) -> Dict[str, str]:
127
+ labels = {}
128
+ for frame in self._frames:
129
+ if frame:
130
+ fname = os.path.basename(frame).lower()
131
+ if "fire" in fname:
132
+ labels[frame] = "fire"
133
+ elif "smoke" in fname:
134
+ labels[frame] = "smoke"
135
+ else:
136
+ labels[frame] = "no_fire"
137
+ return labels
138
+
139
+ def _discover_frames(self):
140
+ frames = []
141
+ if self.frames_dir and os.path.isdir(self.frames_dir):
142
+ for fname in sorted(os.listdir(self.frames_dir)):
143
+ if fname.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".gif")):
144
+ frames.append(os.path.join(self.frames_dir, fname))
145
+ if not frames:
146
+ frames = [None]
147
+ return frames
148
+
149
+ def _run_inference(self, frame: np.ndarray) -> Dict[str, Any]:
150
+ if self._inference is not None:
151
+ try:
152
+ cv2_frame = frame[:, :, ::-1].copy() if frame.shape[-1] == 3 else frame
153
+ result = self._inference.predict(cv2_frame)
154
+ class_idx = result["class_idx"]
155
+ conf = result["confidence"]
156
+ gradcam_summary_val = (
157
+ 1 if "hotspot" in result.get("gradcam_summary", "") else 0
158
+ )
159
+ return {
160
+ "class_idx": class_idx,
161
+ "confidence": conf,
162
+ "gradcam": np.array([conf], dtype=np.float32),
163
+ "gradcam_summary_val": gradcam_summary_val,
164
+ "label": result.get("label", self.CLASS_LABELS[class_idx]),
165
+ "frame": frame,
166
+ }
167
+ except Exception:
168
+ pass
169
+
170
+ gray = np.mean(frame) if frame is not None else 127
171
+ if gray > 170:
172
+ class_idx = 1
173
+ conf = 0.92
174
+ elif gray > 85:
175
+ class_idx = 2
176
+ conf = 0.60
177
+ else:
178
+ class_idx = 0
179
+ conf = 0.40
180
+ return {
181
+ "class_idx": class_idx,
182
+ "confidence": conf,
183
+ "gradcam": np.array([conf], dtype=np.float32),
184
+ "gradcam_summary_val": 0,
185
+ "label": self.CLASS_LABELS[class_idx],
186
+ "frame": frame,
187
+ }
188
+
189
+ def _compute_reward(
190
+ self, action: int, inf: Dict[str, Any], ground_truth: str
191
+ ) -> Tuple[float, bool]:
192
+ action_str = self._action_str(action)
193
+ pred_label = inf.get("label", self.CLASS_LABELS[inf["class_idx"]])
194
+ confidence = inf["confidence"]
195
+ gradcam_strong = inf.get("gradcam_summary_val", 0) == 1
196
+
197
+ # Simplified reward - focus on action vs ground_truth, not prediction match
198
+ if ground_truth == "no_fire":
199
+ if action_str in ["Alert", "Deploy"]:
200
+ return -0.75, False
201
+ elif action_str == "Ignore":
202
+ return 0.1, False # Small positive for correct inaction
203
+ else: # Scan
204
+ return 0.0, False
205
+
206
+ if ground_truth in ["fire", "smoke"]:
207
+ if action_str == "Ignore":
208
+ self._missed_count += 1
209
+ return -0.50, self._missed_count >= self.MAX_MISSES
210
+
211
+ if action_str in ["Alert", "Deploy"]:
212
+ # Reward based on confidence regardless of exact prediction match
213
+ if confidence >= 0.85 and gradcam_strong:
214
+ return 2.00, False
215
+ elif confidence >= 0.70:
216
+ return 1.00, False
217
+ else:
218
+ return 0.50, False
219
+
220
+ if action_str == "Scan":
221
+ # Scan is neutral - continue observing
222
+ return 0.0, False
223
+
224
+ return 0.0, False
225
+
226
+ def _get_ground_truth(self, frame_path: Optional[str]) -> str:
227
+ if frame_path and frame_path in self._ground_truth_labels:
228
+ return self._ground_truth_labels[frame_path]
229
+ if frame_path:
230
+ fname = os.path.basename(frame_path).lower()
231
+ if "fire" in fname:
232
+ return "fire"
233
+ elif "smoke" in fname:
234
+ return "smoke"
235
+ return "no_fire"
236
+
237
+ def reset(
238
+ self, seed: Optional[int] = None, options: Optional[Dict] = None
239
+ ) -> Dict[str, Any]:
240
+ if seed is not None:
241
+ np.random.seed(seed)
242
+ self._frame_idx = 0
243
+ self._step = 0
244
+ self._missed_count = 0
245
+ self._cumulative_reward = 0.0
246
+ self._episode_history = []
247
+
248
+ frame_path = self._frames[self._frame_idx] if self._frames else None
249
+ frame = self._load_frame(frame_path)
250
+ if frame is None:
251
+ frame = self._synthetic_frame()
252
+
253
+ inf = self._run_inference(frame)
254
+ ground_truth = self._get_ground_truth(frame_path)
255
+ gradcam_val = inf.get("gradcam_summary_val", 0)
256
+
257
+ obs = {
258
+ "image": frame,
259
+ "prediction": np.array(
260
+ [
261
+ inf["confidence"] if inf["class_idx"] == 1 else 0.0,
262
+ inf["confidence"] if inf["class_idx"] == 2 else 0.0,
263
+ inf["confidence"] if inf["class_idx"] == 0 else 0.0,
264
+ ]
265
+ ).astype(np.float32),
266
+ "gradcam": inf["gradcam"],
267
+ "gradcam_summary": np.array([gradcam_val], dtype=np.int32),
268
+ "frame_id": np.array([self._frame_idx], dtype=np.int32),
269
+ "step": np.array([self._step], dtype=np.int32),
270
+ "ground_truth": np.array(
271
+ ["fire", "smoke", "no_fire"].index(ground_truth), dtype=np.int32
272
+ ),
273
+ }
274
+ return obs
275
+
276
+ def _load_frame(self, path: Optional[str]) -> Optional[np.ndarray]:
277
+ if path is None:
278
+ return None
279
+ try:
280
+ img = (
281
+ Image.open(path)
282
+ .convert("RGB")
283
+ .resize((self._frame_width, self._frame_height))
284
+ )
285
+ return np.asarray(img, dtype=np.uint8)
286
+ except Exception:
287
+ return None
288
+
289
+ def _synthetic_frame(self) -> np.ndarray:
290
+ return np.full((self._frame_height, self._frame_width, 3), 128, dtype=np.uint8)
291
+
292
+ def step(self, action: int) -> Tuple[Dict[str, Any], float, bool, Dict[str, Any]]:
293
+ assert 0 <= int(action) < len(self.ACTIONS), "Invalid action"
294
+ self._step += 1
295
+ self._frame_idx += 1
296
+
297
+ frame_path = (
298
+ self._frames[self._frame_idx]
299
+ if self._frame_idx < self._max_frames
300
+ else None
301
+ )
302
+ frame = self._load_frame(frame_path)
303
+ if frame is None:
304
+ frame = self._synthetic_frame()
305
+
306
+ inf = self._run_inference(frame)
307
+ ground_truth = self._get_ground_truth(frame_path)
308
+ gradcam_val = inf.get("gradcam_summary_val", 0)
309
+
310
+ reward, terminal_failure = self._compute_reward(action, inf, ground_truth)
311
+ self._cumulative_reward += reward
312
+
313
+ self._episode_history.append(
314
+ {
315
+ "step": self._step,
316
+ "frame_id": self._frame_idx,
317
+ "action": self._action_str(action),
318
+ "prediction": inf.get("label", self.CLASS_LABELS[inf["class_idx"]]),
319
+ "confidence": inf["confidence"],
320
+ "ground_truth": ground_truth,
321
+ "reward": reward,
322
+ }
323
+ )
324
+
325
+ done = terminal_failure or self._frame_idx >= self._max_frames
326
+
327
+ obs = {
328
+ "image": frame,
329
+ "prediction": np.array(
330
+ [
331
+ inf["confidence"] if inf["class_idx"] == 1 else 0.0,
332
+ inf["confidence"] if inf["class_idx"] == 2 else 0.0,
333
+ inf["confidence"] if inf["class_idx"] == 0 else 0.0,
334
+ ]
335
+ ).astype(np.float32),
336
+ "gradcam": inf["gradcam"],
337
+ "gradcam_summary": np.array([gradcam_val], dtype=np.int32),
338
+ "frame_id": np.array([self._frame_idx], dtype=np.int32),
339
+ "step": np.array([self._step], dtype=np.int32),
340
+ "ground_truth": np.array(
341
+ ["fire", "smoke", "no_fire"].index(ground_truth), dtype=np.int32
342
+ ),
343
+ }
344
+
345
+ info = {
346
+ "action": self._action_str(action),
347
+ "reward": reward,
348
+ "cumulative_reward": self._cumulative_reward,
349
+ "prediction": inf.get("label", self.CLASS_LABELS[inf["class_idx"]]),
350
+ "ground_truth": ground_truth,
351
+ "confidence": inf["confidence"],
352
+ "terminal": terminal_failure,
353
+ }
354
+
355
+ if done:
356
+ self._save_episode_log()
357
+
358
+ return obs, reward, done, info
359
+
360
+ def _save_episode_log(self):
361
+ try:
362
+ with open(self.LOG_FILE, "a") as f:
363
+ f.write(f"\n=== Episode ===\n")
364
+ f.write(f"Total frames: {len(self._episode_history)}\n")
365
+ f.write(f"Cumulative reward: {self._cumulative_reward:.2f}\n")
366
+ f.write(f"Success: {self._is_success()}\n")
367
+ for entry in self._episode_history:
368
+ f.write(f" Step {entry['step']}: {entry}\n")
369
+ except Exception:
370
+ pass
371
+
372
+ def _is_success(self) -> bool:
373
+ for entry in self._episode_history:
374
+ gt = entry["ground_truth"]
375
+ action = entry["action"]
376
+ if gt in ["fire", "smoke"] and action not in ["Alert", "Deploy"]:
377
+ return False
378
+ if gt == "no_fire" and action in ["Alert", "Deploy"]:
379
+ return False
380
+ return True
381
+
382
+ def grade(self) -> float:
383
+ if not self._episode_history:
384
+ return 0.0
385
+ if self._is_success():
386
+ return 1.0
387
+ return max(0.0, self._cumulative_reward / 10.0)
388
+
389
+ def render(self, mode: str = "human") -> Optional[Any]:
390
+ return None
391
+
392
+ def close(self) -> None:
393
+ pass
inference.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Wildfire Detection Inference Script
3
+ ====================================
4
+ For LOCAL TESTING: Uses trained RL model (no API key required)
5
+ For SUBMISSION: Uses OpenAI client with API key injected by judges
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import base64
11
+ import pickle
12
+ from io import BytesIO
13
+ from typing import List, Optional, Dict, Any
14
+ from collections import defaultdict
15
+
16
+ from openai import OpenAI
17
+ import numpy as np
18
+ from PIL import Image
19
+
20
+ # Environment variables
21
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
22
+ MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-3B-Instruct")
23
+ API_KEY = os.getenv("API_KEY")
24
+
25
+ # Check if we can use LLM mode
26
+ USE_LLM = API_KEY is not None
27
+
28
+ if USE_LLM:
29
+ client = OpenAI(base_url=API_BASE_URL, api_key=API_KEY)
30
+ else:
31
+ client = None
32
+ print("NOTE: Running in LOCAL MODE (no API_KEY). Using trained RL model.")
33
+
34
+
35
+ # RL Model class for local mode
36
+ class TrainedQLearningAgent:
37
+ """Trained Q-learning agent for local inference."""
38
+
39
+ def __init__(self):
40
+ model_path = os.path.join(os.path.dirname(__file__), "q_model.pkl")
41
+ if os.path.exists(model_path):
42
+ with open(model_path, "rb") as f:
43
+ self.q_table = defaultdict(lambda: [0.0, 0.0, 0.0, 0.0], pickle.load(f))
44
+ print(f"Loaded RL model from: {model_path}")
45
+ else:
46
+ print("WARNING: No trained model found, using fallback!")
47
+ self.q_table = defaultdict(lambda: [0.0, 0.0, 0.0, 0.0])
48
+ self.actions = ["Alert", "Scan", "Ignore", "Deploy"]
49
+
50
+ def get_state_key(self, obs):
51
+ prediction = obs.get("prediction", np.zeros(3))
52
+ if hasattr(prediction, "tolist"):
53
+ p = prediction.tolist()
54
+ else:
55
+ p = list(prediction)
56
+ fire_high = 0 if p[0] < 0.5 else 1
57
+ smoke_high = 0 if p[1] < 0.5 else 1
58
+ return (fire_high, smoke_high)
59
+
60
+ def choose_action(self, state):
61
+ q_values = self.q_table.get(state, [0.0, 0.0, 0.0, 0.0])
62
+ if hasattr(q_values, "tolist"):
63
+ q_values = q_values.tolist()
64
+ return q_values.index(max(q_values))
65
+
66
+ def get_action_name(self, idx):
67
+ return self.actions[idx]
68
+
69
+
70
+ # Import environment
71
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
72
+ from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
73
+
74
+
75
+ def encode_image_to_base64(image_array: np.ndarray) -> str:
76
+ """Convert numpy image to base64 data URI."""
77
+ image = Image.fromarray(image_array)
78
+ buffer = BytesIO()
79
+ image.save(buffer, format="JPEG", quality=85)
80
+ buffer.seek(0)
81
+ data = base64.b64encode(buffer.read()).decode("utf-8")
82
+ return f"data:image/jpeg;base64,{data}"
83
+
84
+
85
+ def build_observation_prompt(step: int, obs: Dict[str, Any]) -> str:
86
+ """Build user prompt from observation."""
87
+ prediction = obs.get("prediction", {})
88
+ fire_prob = prediction[0] if hasattr(prediction, "__getitem__") else 0
89
+ smoke_prob = prediction[1] if hasattr(prediction, "__getitem__") else 0
90
+ no_fire_prob = prediction[2] if hasattr(prediction, "__getitem__") else 0
91
+
92
+ gradcam = obs.get("gradcam_summary", "N/A")
93
+ frame_id = (
94
+ obs.get("frame_id", [0])[0]
95
+ if hasattr(obs.get("frame_id", [0]), "__len__")
96
+ else 0
97
+ )
98
+
99
+ prompt = f"""Frame {frame_id} - Step {step}
100
+ Prediction probabilities:
101
+ - Fire: {fire_prob:.2%}
102
+ - Smoke: {smoke_prob:.2%}
103
+ - No Fire: {no_fire_prob:.2%}
104
+
105
+ Grad-CAM analysis: {gradcam}
106
+
107
+ What is your action? (Alert/Scan/Ignore/Deploy)
108
+ """
109
+ return prompt.strip()
110
+
111
+
112
+ def parse_action(response_text: str) -> str:
113
+ """Parse LLM response to extract action."""
114
+ if not response_text:
115
+ return "Scan"
116
+ response = response_text.strip().lower()
117
+ if "alert" in response:
118
+ return "Alert"
119
+ elif "deploy" in response:
120
+ return "Deploy"
121
+ elif "ignore" in response:
122
+ return "Ignore"
123
+ return "Scan"
124
+
125
+
126
+ def action_to_index(action: str) -> int:
127
+ actions = ["Alert", "Scan", "Ignore", "Deploy"]
128
+ return actions.index(action) if action in actions else 1
129
+
130
+
131
+ MAX_STEPS = 20
132
+ TEMPERATURE = 0.2
133
+ MAX_TOKENS = 500
134
+
135
+ SYSTEM_PROMPT = """You are an autonomous wildfire detection agent. You control a drone monitoring for fires and smoke.
136
+
137
+ Given camera frame observations with prediction probabilities and Grad-CAM analysis, decide the best action.
138
+
139
+ Actions available:
140
+ - Alert: Raise immediate alert if fire/smoke detected with high confidence
141
+ - Scan: Continue scanning/observing the current frame more carefully
142
+ - Ignore: No action needed, clear conditions
143
+ - Deploy: Deploy resources to the detected location
144
+
145
+ Output ONLY the action name: Alert, Scan, Ignore, or Deploy
146
+
147
+ Consider:
148
+ - Prediction confidence levels
149
+ - Grad-CAM heatmap alignment (strong hotspot = more reliable detection)
150
+ - Balance early detection vs false alarms
151
+ - Sequential frame context
152
+ """
153
+
154
+
155
+ def run_episode(env) -> Dict[str, Any]:
156
+ """Run a single episode and return results."""
157
+ rewards_list = []
158
+ steps_executed = 0
159
+
160
+ # Use appropriate model name based on mode
161
+ model_used = MODEL_NAME if USE_LLM else "trained_q_learning"
162
+ print(f"[START] task=wildfire_detection env=wildfire_detection model={model_used}")
163
+
164
+ # Initialize RL agent for local mode
165
+ rl_agent = TrainedQLearningAgent() if not USE_LLM else None
166
+
167
+ obs = env.reset()
168
+
169
+ for step in range(1, MAX_STEPS + 1):
170
+ steps_executed = step
171
+
172
+ if USE_LLM:
173
+ # LLM mode
174
+ user_prompt = build_observation_prompt(step, obs)
175
+
176
+ image_b64 = ""
177
+ if "image" in obs and obs["image"] is not None:
178
+ image_b64 = encode_image_to_base64(obs["image"])
179
+
180
+ messages = [
181
+ {"role": "system", "content": SYSTEM_PROMPT},
182
+ {
183
+ "role": "user",
184
+ "content": [
185
+ {"type": "text", "text": user_prompt},
186
+ {"type": "image_url", "image_url": {"url": image_b64}}
187
+ if image_b64
188
+ else {"type": "text", "text": "(no image)"},
189
+ ],
190
+ },
191
+ ]
192
+
193
+ try:
194
+ completion = client.chat.completions.create(
195
+ model=MODEL_NAME,
196
+ messages=messages,
197
+ temperature=TEMPERATURE,
198
+ max_tokens=MAX_TOKENS,
199
+ stream=False,
200
+ )
201
+ response_text = completion.choices[0].message.content or ""
202
+ except Exception as e:
203
+ print(f"Model request failed: {e}. Using fallback.")
204
+ response_text = "Scan"
205
+
206
+ action_str = parse_action(response_text)
207
+ else:
208
+ # RL mode
209
+ state = rl_agent.get_state_key(obs)
210
+ action_idx = rl_agent.choose_action(state)
211
+ action_str = rl_agent.get_action_name(action_idx)
212
+
213
+ action_idx = action_to_index(action_str)
214
+
215
+ obs, reward, done, info = env.step(action_idx)
216
+ rewards_list.append(reward)
217
+
218
+ error_msg = "null"
219
+ print(
220
+ f"[STEP] step={step} action={action_str} reward={reward:.2f} done={str(done).lower()} error={error_msg}"
221
+ )
222
+
223
+ if done:
224
+ break
225
+
226
+ total_reward = sum(rewards_list)
227
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards_list)
228
+ success = total_reward > 0
229
+
230
+ print(
231
+ f"[END] success={str(success).lower()} steps={steps_executed} rewards={rewards_str}"
232
+ )
233
+
234
+ return {
235
+ "success": success,
236
+ "steps": steps_executed,
237
+ "rewards": rewards_list,
238
+ "total_reward": total_reward,
239
+ }
240
+
241
+
242
+ def main():
243
+ """Main entry point."""
244
+ model_path = os.path.join(
245
+ os.path.dirname(__file__),
246
+ "FirenetCNN1.h5",
247
+ )
248
+
249
+ env = WildfireDetectionEnv(model_path=model_path)
250
+
251
+ try:
252
+ result = run_episode(env)
253
+ print(
254
+ f"Episode completed: success={result['success']}, steps={result['steps']}, total_reward={result['total_reward']:.2f}"
255
+ )
256
+ finally:
257
+ env.close()
258
+
259
+
260
+ if __name__ == "__main__":
261
+ main()
inference_rl.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Wildfire Detection RL Inference Script
3
+ =======================================
4
+ Uses trained Q-learning model for decision making.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import pickle
10
+ import numpy as np
11
+ from collections import defaultdict
12
+
13
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14
+
15
+ from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
16
+
17
+
18
+ class TrainedQLearningAgent:
19
+ """Trained Q-learning agent for inference."""
20
+
21
+ def __init__(self, model_path=None):
22
+ self.model_path = model_path or os.path.join(
23
+ os.path.dirname(__file__), "q_model.pkl"
24
+ )
25
+ self.q_table = None
26
+ self.actions = ["Alert", "Scan", "Ignore", "Deploy"]
27
+ self._load_model()
28
+
29
+ def _load_model(self):
30
+ if os.path.exists(self.model_path):
31
+ with open(self.model_path, "rb") as f:
32
+ self.q_table = defaultdict(lambda: [0.0, 0.0, 0.0, 0.0], pickle.load(f))
33
+ print(f"Loaded trained model from: {self.model_path}")
34
+ print(f"Q-table: {dict(self.q_table)}")
35
+ else:
36
+ print("WARNING: No trained model found!")
37
+ self.q_table = defaultdict(lambda: [0.0, 0.0, 0.0, 0.0])
38
+
39
+ def get_state_key(self, obs):
40
+ """Convert observation to discrete state key."""
41
+ prediction = obs.get("prediction", np.zeros(3))
42
+ if hasattr(prediction, "tolist"):
43
+ p = prediction.tolist()
44
+ elif hasattr(prediction, "__iter__"):
45
+ p = list(prediction)
46
+ else:
47
+ p = [0, 0, 0]
48
+
49
+ # Match training: fire_high, smoke_high
50
+ fire_high = 0 if p[0] < 0.5 else 1
51
+ smoke_high = 0 if p[1] < 0.5 else 1
52
+
53
+ return (fire_high, smoke_high)
54
+
55
+ def choose_action(self, state):
56
+ """Choose best action based on Q-values."""
57
+ q_values = self.q_table.get(state, [0.0, 0.0, 0.0, 0.0])
58
+ if hasattr(q_values, "tolist"):
59
+ q_values = q_values.tolist()
60
+ return q_values.index(max(q_values))
61
+
62
+ def get_action_name(self, action_idx):
63
+ return self.actions[action_idx]
64
+
65
+
66
+ def run_inference():
67
+ """Run inference with trained RL agent."""
68
+ print(
69
+ "[START] task=wildfire_detection env=wildfire_detection model=trained_q_learning"
70
+ )
71
+
72
+ env = WildfireDetectionEnv()
73
+ agent = TrainedQLearningAgent()
74
+
75
+ rewards_list = []
76
+ steps_executed = 0
77
+
78
+ obs = env.reset()
79
+
80
+ for step in range(1, 21):
81
+ steps_executed = step
82
+
83
+ state = agent.get_state_key(obs)
84
+ action_idx = agent.choose_action(state)
85
+ action_str = agent.get_action_name(action_idx)
86
+
87
+ obs, reward, done, info = env.step(action_idx)
88
+ rewards_list.append(reward)
89
+
90
+ error_msg = "null"
91
+ print(
92
+ f"[STEP] step={step} action={action_str} reward={reward:.2f} done={str(done).lower()} error={error_msg}"
93
+ )
94
+
95
+ if done:
96
+ break
97
+
98
+ env.close()
99
+
100
+ total_reward = sum(rewards_list)
101
+ rewards_str = ",".join(f"{r:.2f}" for r in rewards_list)
102
+ success = total_reward > 0
103
+
104
+ print(
105
+ f"[END] success={str(success).lower()} steps={steps_executed} rewards={rewards_str}"
106
+ )
107
+
108
+ return {
109
+ "success": success,
110
+ "steps": steps_executed,
111
+ "rewards": rewards_list,
112
+ "total_reward": total_reward,
113
+ }
114
+
115
+
116
+ if __name__ == "__main__":
117
+ result = run_inference()
118
+ print(
119
+ f"\nResult: success={result['success']}, total_reward={result['total_reward']:.2f}"
120
+ )
models.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Data models for the Wildfire Detection Environment.
9
+ """
10
+
11
+ from openenv.core.env_server.types import Action, Observation
12
+ from pydantic import Field
13
+ from typing import Optional, Dict, Any
14
+
15
+
16
+ class WildfireAction(Action):
17
+ """Action for the Wildfire Detection environment."""
18
+
19
+ action: str = Field(
20
+ ..., description="Action to take: Alert, Scan, Ignore, or Deploy"
21
+ )
22
+
23
+
24
+ class WildfireObservation(Observation):
25
+ """Observation from the Wildfire Detection environment."""
26
+
27
+ image: str = Field(default="", description="Base64 encoded image or file path")
28
+ prediction: Dict[str, float] = Field(
29
+ default_factory=dict,
30
+ description="Prediction probabilities for fire, smoke, no_fire",
31
+ )
32
+ gradcam_summary: str = Field(default="", description="Grad-CAM heatmap summary")
33
+ frame_id: int = Field(default=0, description="Current frame ID")
34
+ step: int = Field(default=0, description="Current step")
35
+ ground_truth: str = Field(
36
+ default="", description="Ground truth label (for training)"
37
+ )
38
+ reward: float = Field(default=0.0, description="Reward for the last action")
39
+ done: bool = Field(default=False, description="Whether episode is done")
40
+ metadata: Dict[str, Any] = Field(
41
+ default_factory=dict, description="Additional metadata"
42
+ )
openenv.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ spec_version: 1
2
+ name: wildfire_detection
3
+ type: space
4
+ runtime: fastapi
5
+ app: run_server:main
6
+ port: 8000
openenv_wildfire_detection.egg-info/PKG-INFO ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: openenv-wildfire-detection
3
+ Version: 0.1.0
4
+ Summary: Wildfire Detection RL Environment for OpenEnv
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: openenv-core[core]>=0.2.2
7
+ Requires-Dist: numpy>=1.19.0
8
+ Requires-Dist: pillow>=10.0.0
9
+ Requires-Dist: opencv-python>=4.8.0
10
+ Requires-Dist: tensorflow>=2.15.0
11
+ Requires-Dist: gymnasium>=0.29.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
14
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
openenv_wildfire_detection.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ README.md
2
+ __init__.py
3
+ client.py
4
+ inference.py
5
+ models.py
6
+ pyproject.toml
7
+ run_server.py
8
+ test_env.py
9
+ ./__init__.py
10
+ ./client.py
11
+ ./debug_env.py
12
+ ./inference.py
13
+ ./inference_rl.py
14
+ ./models.py
15
+ ./run_server.py
16
+ ./test_env.py
17
+ ./train_rl.py
18
+ environments/wildfire_detection/__init__.py
19
+ environments/wildfire_detection/firenet_inference.py
20
+ environments/wildfire_detection/wildfire_env.py
21
+ openenv_wildfire_detection.egg-info/PKG-INFO
22
+ openenv_wildfire_detection.egg-info/SOURCES.txt
23
+ openenv_wildfire_detection.egg-info/dependency_links.txt
24
+ openenv_wildfire_detection.egg-info/entry_points.txt
25
+ openenv_wildfire_detection.egg-info/requires.txt
26
+ openenv_wildfire_detection.egg-info/top_level.txt
27
+ server/__init__.py
28
+ server/app.py
29
+ server/multipen_environment.py
30
+ server/wildfire_environment.py
openenv_wildfire_detection.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
openenv_wildfire_detection.egg-info/entry_points.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [console_scripts]
2
+ server = multipen.server.app:main
openenv_wildfire_detection.egg-info/requires.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ openenv-core[core]>=0.2.2
2
+ numpy>=1.19.0
3
+ pillow>=10.0.0
4
+ opencv-python>=4.8.0
5
+ tensorflow>=2.15.0
6
+ gymnasium>=0.29.0
7
+
8
+ [dev]
9
+ pytest>=8.0.0
10
+ pytest-cov>=4.0.0
openenv_wildfire_detection.egg-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ multipen
push_hf.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ os.environ["PYTHONIOENCODING"] = "utf-8"
4
+ import subprocess
5
+
6
+ result = subprocess.run(
7
+ [
8
+ "python",
9
+ "-m",
10
+ "openenv.cli",
11
+ "push",
12
+ "--repo-id",
13
+ "OpelSpeedster/Wildfire-Detection_Openenv",
14
+ ],
15
+ cwd=r"C:\Users\ASUS\OneDrive\Desktop\RL\multipen",
16
+ capture_output=True,
17
+ text=True,
18
+ encoding="utf-8",
19
+ )
20
+ print("STDOUT:", result.stdout)
21
+ print("STDERR:", result.stderr)
pyproject.toml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ [build-system]
8
+ requires = ["setuptools>=45", "wheel"]
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "openenv-wildfire-detection"
13
+ version = "0.1.0"
14
+ description = "Wildfire Detection RL Environment for OpenEnv"
15
+ requires-python = ">=3.10"
16
+ dependencies = [
17
+ # Core OpenEnv runtime
18
+ "openenv-core[core]>=0.2.2",
19
+ # Environment dependencies
20
+ "numpy>=1.19.0",
21
+ "pillow>=10.0.0",
22
+ "opencv-python>=4.8.0",
23
+ "tensorflow>=2.15.0",
24
+ # RL dependencies
25
+ "gymnasium>=0.29.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0.0",
31
+ "pytest-cov>=4.0.0",
32
+ ]
33
+
34
+ [project.scripts]
35
+ server = "multipen.server.app:main"
36
+
37
+ [tool.setuptools]
38
+ include-package-data = true
39
+ packages = ["multipen", "multipen.server", "multipen.environments.wildfire_detection"]
40
+ package-dir = { "multipen" = ".", "multipen.server" = "server", "multipen.environments.wildfire_detection" = "environments/wildfire_detection" }
q_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ade681309aa444dc7029e5afe08520935fdec605afbcde5d3bc18ea3f553ee24
3
+ size 326
requirements.txt ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofile==3.9.0
2
+ aiofiles==24.1.0
3
+ annotated-doc==0.0.4
4
+ annotated-types==0.7.0
5
+ anyio==4.13.0
6
+ attrs==26.1.0
7
+ Authlib==1.6.9
8
+ beartype==0.22.9
9
+ brotli==1.2.0
10
+ cachetools==7.0.5
11
+ caio==0.9.25
12
+ certifi==2026.2.25
13
+ cffi==2.0.0
14
+ charset-normalizer==3.4.7
15
+ click==8.3.2
16
+ cryptography==46.0.7
17
+ cyclopts==4.10.1
18
+ distro==1.9.0
19
+ dnspython==2.8.0
20
+ docstring_parser==0.17.0
21
+ docutils==0.22.4
22
+ email-validator==2.3.0
23
+ exceptiongroup==1.3.1
24
+ fastapi==0.135.3
25
+ fastmcp==3.2.0
26
+ ffmpy==1.0.0
27
+ filelock==3.25.2
28
+ fsspec==2026.3.0
29
+ gradio==6.11.0
30
+ gradio_client==2.4.0
31
+ groovy==0.1.2
32
+ h11==0.16.0
33
+ hf-gradio==0.3.0
34
+ hf-xet==1.4.3
35
+ httpcore==1.0.9
36
+ httpx==0.28.1
37
+ httpx-sse==0.4.3
38
+ huggingface_hub==1.9.2
39
+ idna==3.11
40
+ importlib_metadata==8.7.1
41
+ jaraco.classes==3.4.0
42
+ jaraco.context==6.1.2
43
+ jaraco.functools==4.4.0
44
+ jeepney==0.9.0
45
+ Jinja2==3.1.6
46
+ jiter==0.13.0
47
+ jsonref==1.1.0
48
+ jsonschema==4.26.0
49
+ jsonschema-path==0.4.5
50
+ jsonschema-specifications==2025.9.1
51
+ keyring==25.7.0
52
+ markdown-it-py==4.0.0
53
+ MarkupSafe==3.0.3
54
+ mcp==1.27.0
55
+ mdurl==0.1.2
56
+ more-itertools==11.0.1
57
+ numpy==2.4.4
58
+ openai==2.30.0
59
+ openapi-pydantic==0.5.1
60
+ openenv-core==0.2.3
61
+ opentelemetry-api==1.40.0
62
+ orjson==3.11.8
63
+ packaging==26.0
64
+ pandas==3.0.2
65
+ pathable==0.5.0
66
+ pillow==12.2.0
67
+ platformdirs==4.9.4
68
+ py-key-value-aio==0.4.4
69
+ pycparser==3.0
70
+ pydantic==2.12.5
71
+ pydantic-settings==2.13.1
72
+ pydantic_core==2.41.5
73
+ pydub==0.25.1
74
+ Pygments==2.20.0
75
+ PyJWT==2.12.1
76
+ pyperclip==1.11.0
77
+ python-dateutil==2.9.0.post0
78
+ python-dotenv==1.2.2
79
+ python-multipart==0.0.24
80
+ pytz==2026.1.post1
81
+ PyYAML==6.0.3
82
+ referencing==0.37.0
83
+ requests==2.33.1
84
+ rich==14.3.3
85
+ rich-rst==1.3.2
86
+ rpds-py==0.30.0
87
+ safehttpx==0.1.7
88
+ SecretStorage==3.5.0
89
+ semantic-version==2.10.0
90
+ shellingham==1.5.4
91
+ six==1.17.0
92
+ sniffio==1.3.1
93
+ sse-starlette==3.3.4
94
+ starlette==1.0.0
95
+ tomli==2.4.1
96
+ tomli_w==1.2.0
97
+ tomlkit==0.13.3
98
+ tqdm==4.67.3
99
+ typer==0.24.1
100
+ typing-inspection==0.4.2
101
+ typing_extensions==4.15.0
102
+ uncalled-for==0.3.1
103
+ urllib3==2.6.3
104
+ uvicorn==0.44.0
105
+ watchfiles==1.1.1
106
+ websockets==16.0
107
+ zipp==3.23.0
run_server.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import uvicorn
2
+ from server.app import app
3
+
4
+ if __name__ == "__main__":
5
+ print("Starting Wildfire Detection OpenEnv Server...")
6
+ print("Server will be available at http://localhost:8000")
7
+ uvicorn.run(app, host="0.0.0.0", port=8000)
server/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """Wildfire Detection environment server components."""
8
+
9
+ from .wildfire_environment import WildfireEnvironment
10
+
11
+ __all__ = ["WildfireEnvironment"]
server/app.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ FastAPI application for the Wildfire Detection Environment.
9
+
10
+ This module creates an HTTP server that exposes the WildfireEnvironment
11
+ over HTTP and WebSocket endpoints, compatible with EnvClient.
12
+
13
+ Endpoints:
14
+ - POST /reset: Reset the environment
15
+ - POST /step: Execute an action
16
+ - GET /state: Get current environment state
17
+ - GET /schema: Get action/observation schemas
18
+ - WS /ws: WebSocket endpoint for persistent sessions
19
+
20
+ Usage:
21
+ # Development (with auto-reload):
22
+ uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
23
+
24
+ # Production:
25
+ uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
26
+
27
+ # Or run directly:
28
+ python -m server.app
29
+ """
30
+
31
+ try:
32
+ from openenv.core.env_server.http_server import create_app
33
+ except Exception as e:
34
+ raise ImportError("openenv is required for the web interface.") from e
35
+
36
+ try:
37
+ from models import WildfireAction, WildfireObservation
38
+ except (ModuleNotFoundError, ImportError):
39
+ from multipen.models import WildfireAction, WildfireObservation
40
+
41
+ try:
42
+ from server.wildfire_environment import WildfireEnvironment
43
+ except (ModuleNotFoundError, ImportError):
44
+ from multipen.server.wildfire_environment import WildfireEnvironment
45
+
46
+
47
+ app = create_app(
48
+ WildfireEnvironment,
49
+ WildfireAction,
50
+ WildfireObservation,
51
+ env_name="wildfire_detection",
52
+ max_concurrent_envs=1,
53
+ )
54
+
55
+
56
+ def main():
57
+ """Entry point for direct execution."""
58
+ import uvicorn
59
+
60
+ uvicorn.run(app, host="0.0.0.0", port=8000)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
server/multipen_environment.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Multipen Environment Implementation.
9
+
10
+ A simple test environment that echoes back messages sent to it.
11
+ Perfect for testing HTTP server infrastructure.
12
+ """
13
+
14
+ from uuid import uuid4
15
+
16
+ from openenv.core.env_server.interfaces import Environment
17
+ from openenv.core.env_server.types import State
18
+
19
+ try:
20
+ from ..models import MultipenAction, MultipenObservation
21
+ except ImportError:
22
+ from models import MultipenAction, MultipenObservation
23
+
24
+
25
+ class MultipenEnvironment(Environment):
26
+ """
27
+ A simple echo environment that echoes back messages.
28
+
29
+ This environment is designed for testing the HTTP server infrastructure.
30
+ It maintains minimal state and simply echoes back whatever message it receives.
31
+
32
+ Example:
33
+ >>> env = MultipenEnvironment()
34
+ >>> obs = env.reset()
35
+ >>> print(obs.echoed_message) # "Multipen environment ready!"
36
+ >>>
37
+ >>> obs = env.step(MultipenAction(message="Hello"))
38
+ >>> print(obs.echoed_message) # "Hello"
39
+ >>> print(obs.message_length) # 5
40
+ """
41
+
42
+ # Enable concurrent WebSocket sessions.
43
+ # Set to True if your environment isolates state between instances.
44
+ # When True, multiple WebSocket clients can connect simultaneously, each
45
+ # getting their own environment instance (when using factory mode in app.py).
46
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
47
+
48
+ def __init__(self):
49
+ """Initialize the multipen environment."""
50
+ self._state = State(episode_id=str(uuid4()), step_count=0)
51
+ self._reset_count = 0
52
+
53
+ def reset(self) -> MultipenObservation:
54
+ """
55
+ Reset the environment.
56
+
57
+ Returns:
58
+ MultipenObservation with a ready message
59
+ """
60
+ self._state = State(episode_id=str(uuid4()), step_count=0)
61
+ self._reset_count += 1
62
+
63
+ return MultipenObservation(
64
+ echoed_message="Multipen environment ready!",
65
+ message_length=0,
66
+ done=False,
67
+ reward=0.0,
68
+ )
69
+
70
+ def step(self, action: MultipenAction) -> MultipenObservation: # type: ignore[override]
71
+ """
72
+ Execute a step in the environment by echoing the message.
73
+
74
+ Args:
75
+ action: MultipenAction containing the message to echo
76
+
77
+ Returns:
78
+ MultipenObservation with the echoed message and its length
79
+ """
80
+ self._state.step_count += 1
81
+
82
+ message = action.message
83
+ length = len(message)
84
+
85
+ # Simple reward: longer messages get higher rewards
86
+ reward = length * 0.1
87
+
88
+ return MultipenObservation(
89
+ echoed_message=message,
90
+ message_length=length,
91
+ done=False,
92
+ reward=reward,
93
+ metadata={"original_message": message, "step": self._state.step_count},
94
+ )
95
+
96
+ @property
97
+ def state(self) -> State:
98
+ """
99
+ Get the current environment state.
100
+
101
+ Returns:
102
+ Current State with episode_id and step_count
103
+ """
104
+ return self._state
server/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ openenv-core[core]>=0.2.2
2
+ numpy>=1.19.0
3
+ pillow>=10.0.0
4
+ opencv-python>=4.8.0
5
+ tensorflow>=2.15.0
6
+ gymnasium>=0.29.0
7
+ fastapi>=0.104.0
8
+ uvicorn>=0.24.0
9
+ pydantic>=2.0.0
server/wildfire_environment.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ """
8
+ Wildfire Detection Environment Implementation.
9
+
10
+ This environment uses the FirenetCNN model for wildfire detection.
11
+ """
12
+
13
+ import os
14
+ import base64
15
+ from uuid import uuid4
16
+ from io import BytesIO
17
+
18
+ from openenv.core.env_server.interfaces import Environment
19
+ from openenv.core.env_server.types import State
20
+
21
+ try:
22
+ from ..models import WildfireAction, WildfireObservation
23
+ except ImportError:
24
+ from models import WildfireAction, WildfireObservation
25
+
26
+ try:
27
+ from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
28
+ except ModuleNotFoundError:
29
+ from multipen.environments.wildfire_detection.wildfire_env import (
30
+ WildfireDetectionEnv,
31
+ )
32
+
33
+
34
+ class WildfireEnvironment(Environment):
35
+ """OpenEnv environment for wildfire detection using FirenetCNN."""
36
+
37
+ SUPPORTS_CONCURRENT_SESSIONS: bool = True
38
+
39
+ def __init__(self):
40
+ self._state = State(episode_id=str(uuid4()), step_count=0)
41
+ self._env = None
42
+ self._init_env()
43
+
44
+ def _init_env(self):
45
+ model_path = os.path.join(
46
+ os.path.dirname(__file__),
47
+ "..",
48
+ "..",
49
+ "Forest-Fire-Detection-Using-FirenetCNN-and-XAI-Techniques",
50
+ "FirenetCNN1.h5",
51
+ )
52
+ self._env = WildfireDetectionEnv(model_path=model_path)
53
+
54
+ def reset(self) -> WildfireObservation:
55
+ self._state = State(episode_id=str(uuid4()), step_count=0)
56
+ obs = self._env.reset()
57
+
58
+ return self._make_observation(obs, 0.0, False)
59
+
60
+ def step(self, action: WildfireAction) -> WildfireObservation:
61
+ self._state.step_count += 1
62
+
63
+ action_idx = ["Alert", "Scan", "Ignore", "Deploy"].index(action.action)
64
+ obs, reward, done, info = self._env.step(action_idx)
65
+
66
+ return self._make_observation(obs, reward, done, info)
67
+
68
+ def _make_observation(
69
+ self, obs: dict, reward: float, done: bool
70
+ ) -> WildfireObservation:
71
+ img = obs.get("image")
72
+ img_b64 = ""
73
+ if img is not None:
74
+ from PIL import Image
75
+ import numpy as np
76
+
77
+ pil_img = Image.fromarray(img)
78
+ buffer = BytesIO()
79
+ pil_img.save(buffer, format="JPEG")
80
+ img_b64 = base64.b64encode(buffer.getvalue()).decode()
81
+
82
+ return WildfireObservation(
83
+ image=img_b64,
84
+ prediction={
85
+ "fire": float(obs.get("prediction", [0, 0, 0])[0]),
86
+ "smoke": float(obs.get("prediction", [0, 0, 0])[1]),
87
+ "no_fire": float(obs.get("prediction", [0, 0, 0])[2]),
88
+ },
89
+ gradcam_summary=f"Grad-CAM: {obs.get('gradcam_summary', [0])[0]}",
90
+ frame_id=int(obs.get("frame_id", [0])[0]),
91
+ step=int(obs.get("step", [0])[0]),
92
+ ground_truth=["fire", "smoke", "no_fire"][
93
+ int(obs.get("ground_truth", [2])[0])
94
+ ],
95
+ reward=reward,
96
+ done=done,
97
+ metadata={},
98
+ )
99
+
100
+ @property
101
+ def state(self) -> State:
102
+ return self._state
test_env.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+
4
+ sys.path.insert(0, r"C:\Users\ASUS\OneDrive\Desktop\RL\multipen")
5
+
6
+ from environments.wildfire_detection.wildfire_env import WildfireDetectionEnv
7
+
8
+ print("Creating environment...")
9
+ env = WildfireDetectionEnv()
10
+ print("Environment created successfully")
11
+
12
+ obs = env.reset()
13
+ print(f"Reset successful, step: {obs['step']}")
14
+
15
+ action = 0 # Alert
16
+ obs, reward, done, info = env.step(action)
17
+ print(f"Step taken: action={info['action']}, reward={reward}, done={done}")
18
+
19
+ print("Environment test passed!")
train_rl.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Wildfire Detection RL Training (Fast Version)
3
+ ==============================================
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import numpy as np
9
+ from collections import defaultdict
10
+ import pickle
11
+
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+
14
+ # Pre-computed frame data to avoid slow TensorFlow loading
15
+ FRAME_DATA = [
16
+ {"prediction": [0.9, 0.1, 0.0], "ground_truth": 0, "frame": 0}, # fire
17
+ {"prediction": [0.85, 0.15, 0.0], "ground_truth": 0, "frame": 1}, # fire
18
+ {"prediction": [0.8, 0.2, 0.0], "ground_truth": 0, "frame": 2}, # fire
19
+ {"prediction": [0.7, 0.25, 0.05], "ground_truth": 0, "frame": 3}, # fire
20
+ {"prediction": [0.6, 0.3, 0.1], "ground_truth": 0, "frame": 4}, # fire
21
+ {"prediction": [0.5, 0.4, 0.1], "ground_truth": 1, "frame": 5}, # smoke
22
+ {"prediction": [0.3, 0.6, 0.1], "ground_truth": 1, "frame": 6}, # smoke
23
+ {"prediction": [0.2, 0.7, 0.1], "ground_truth": 1, "frame": 7}, # smoke
24
+ {"prediction": [0.1, 0.8, 0.1], "ground_truth": 1, "frame": 8}, # smoke
25
+ {"prediction": [0.1, 0.85, 0.05], "ground_truth": 1, "frame": 9}, # smoke
26
+ {"prediction": [0.0, 0.1, 0.9], "ground_truth": 2, "frame": 10}, # no_fire
27
+ {"prediction": [0.0, 0.05, 0.95], "ground_truth": 2, "frame": 11}, # no_fire
28
+ {"prediction": [0.05, 0.05, 0.9], "ground_truth": 2, "frame": 12}, # no_fire
29
+ {"prediction": [0.0, 0.0, 1.0], "ground_truth": 2, "frame": 13}, # no_fire
30
+ {"prediction": [0.0, 0.0, 1.0], "ground_truth": 2, "frame": 14}, # no_fire
31
+ {"prediction": [0.9, 0.1, 0.0], "ground_truth": 0, "frame": 15}, # fire (repeat)
32
+ ]
33
+
34
+
35
+ class FastEnv:
36
+ """Fast environment without TensorFlow."""
37
+
38
+ ACTIONS = ["Alert", "Scan", "Ignore", "Deploy"]
39
+ CLASS_LABELS = ["fire", "smoke", "no_fire"]
40
+
41
+ def __init__(self):
42
+ self.frame_idx = 0
43
+ self.n_frames = len(FRAME_DATA)
44
+
45
+ def reset(self):
46
+ self.frame_idx = 0
47
+ return self._get_obs()
48
+
49
+ def step(self, action):
50
+ frame = FRAME_DATA[self.frame_idx]
51
+ gt = frame["ground_truth"]
52
+ action_str = self.ACTIONS[action]
53
+
54
+ # Reward function
55
+ if gt == 2: # no_fire
56
+ if action_str in ["Alert", "Deploy"]:
57
+ reward = -0.75
58
+ elif action_str == "Ignore":
59
+ reward = 0.1
60
+ else:
61
+ reward = 0.0
62
+ else: # fire or smoke
63
+ if action_str == "Ignore":
64
+ reward = -0.50
65
+ elif action_str in ["Alert", "Deploy"]:
66
+ reward = 0.50
67
+ else: # Scan
68
+ reward = 0.0
69
+
70
+ # Move to next frame BEFORE checking done
71
+ self.frame_idx += 1
72
+ done = self.frame_idx >= self.n_frames
73
+
74
+ # If we moved past valid frames, return last observation but mark done
75
+ if done:
76
+ obs = {
77
+ "prediction": np.array([0, 0, 0], dtype=np.float32),
78
+ "ground_truth": np.array([2], dtype=np.int32),
79
+ "frame_id": np.array([self.frame_idx - 1], dtype=np.int32),
80
+ }
81
+ else:
82
+ obs = self._get_obs()
83
+
84
+ return obs, reward, done, {"ground_truth": self.CLASS_LABELS[gt]}
85
+
86
+ def _get_obs(self):
87
+ frame = FRAME_DATA[self.frame_idx]
88
+ return {
89
+ "prediction": np.array(frame["prediction"], dtype=np.float32),
90
+ "ground_truth": np.array([frame["ground_truth"]], dtype=np.int32),
91
+ "frame_id": np.array([self.frame_idx], dtype=np.int32),
92
+ }
93
+
94
+
95
+ class QLearningAgent:
96
+ def __init__(
97
+ self,
98
+ n_actions=4,
99
+ learning_rate=0.4,
100
+ discount_factor=0.9,
101
+ epsilon=1.0,
102
+ epsilon_decay=0.98,
103
+ epsilon_min=0.05,
104
+ ):
105
+ self.n_actions = n_actions
106
+ self.lr = learning_rate
107
+ self.gamma = discount_factor
108
+ self.epsilon = epsilon
109
+ self.epsilon_decay = epsilon_decay
110
+ self.epsilon_min = epsilon_min
111
+ self.q_table = defaultdict(lambda: np.zeros(n_actions))
112
+
113
+ def get_state_key(self, obs):
114
+ p = obs["prediction"]
115
+ fire_high = 0 if p[0] < 0.5 else 1
116
+ smoke_high = 0 if p[1] < 0.5 else 1
117
+ return (fire_high, smoke_high)
118
+
119
+ def choose_action(self, state):
120
+ if np.random.random() < self.epsilon:
121
+ return np.random.randint(self.n_actions)
122
+ return np.argmax(self.q_table[state])
123
+
124
+ def learn(self, state, action, reward, next_state):
125
+ current_q = self.q_table[state][action]
126
+ max_next_q = np.max(self.q_table[next_state])
127
+ td_target = reward + self.gamma * max_next_q
128
+ self.q_table[state][action] = current_q + self.lr * (td_target - current_q)
129
+
130
+ def decay_epsilon(self):
131
+ self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)
132
+
133
+
134
+ def train_agent(num_episodes=200):
135
+ print("Initializing fast environment...")
136
+ env = FastEnv()
137
+
138
+ agent = QLearningAgent()
139
+
140
+ print(f"\nTraining for {num_episodes} episodes...")
141
+
142
+ for episode in range(num_episodes):
143
+ obs = env.reset()
144
+ state = agent.get_state_key(obs)
145
+ total_reward = 0
146
+ done = False
147
+
148
+ while not done:
149
+ action = agent.choose_action(state)
150
+ obs, reward, done, info = env.step(action)
151
+ next_state = agent.get_state_key(obs)
152
+
153
+ agent.learn(state, action, reward, next_state)
154
+ total_reward += reward
155
+ state = next_state
156
+
157
+ agent.decay_epsilon()
158
+
159
+ if (episode + 1) % 20 == 0:
160
+ print(
161
+ f"Episode {episode + 1}/{num_episodes} | Reward: {total_reward:.2f} | Epsilon: {agent.epsilon:.3f}"
162
+ )
163
+
164
+ # Save model
165
+ model_path = os.path.join(os.path.dirname(__file__), "q_model.pkl")
166
+ with open(model_path, "wb") as f:
167
+ pickle.dump(dict(agent.q_table), f)
168
+
169
+ print(f"\nTraining complete!")
170
+ print(f"Q-table: {dict(agent.q_table)}")
171
+ print(f"Model saved to: {model_path}")
172
+
173
+ return agent
174
+
175
+
176
+ def evaluate():
177
+ env = FastEnv()
178
+
179
+ model_path = os.path.join(os.path.dirname(__file__), "q_model.pkl")
180
+ with open(model_path, "rb") as f:
181
+ q_table = pickle.load(f)
182
+
183
+ agent = QLearningAgent()
184
+ agent.q_table = defaultdict(lambda: np.zeros(4), q_table)
185
+ agent.epsilon = 0
186
+
187
+ print("\n=== Evaluation ===")
188
+
189
+ for ep in range(3):
190
+ obs = env.reset()
191
+ state = agent.get_state_key(obs)
192
+ total_reward = 0
193
+ done = False
194
+ actions = []
195
+
196
+ while not done:
197
+ action = agent.choose_action(state)
198
+ actions.append(["Alert", "Scan", "Ignore", "Deploy"][action])
199
+ obs, reward, done, _ = env.step(action)
200
+ total_reward += reward
201
+ state = agent.get_state_key(obs)
202
+
203
+ print(f"Episode: {actions}")
204
+ print(f"Reward: {total_reward:.2f}")
205
+
206
+
207
+ if __name__ == "__main__":
208
+ train_agent(200)
209
+ evaluate()
uv.lock ADDED
The diff for this file is too large to render. See raw diff