Spaces:
Sleeping
Sleeping
Kaushalraj Puwar commited on
Commit ·
e987a94
1
Parent(s): 79412de
refactor(core): integrate modular task system for episode management
Browse filesIntroduce a new task-based architecture with abstract ThermalPlantTask class and specific implementations (task1-4) to govern episode disturbances, completions, and policies. Update env/core.py to delegate max_steps, resets, and disturbances to tasks, enhancing modularity and deterministic behavior. Refine transitions.py with improved reward smoothing and initial state clamping for observable gaps. Modify inference.py to embed task descriptions in LLM prompts for better guidance. Add comprehensive unit and integration tests for task functionality.
BREAKING CHANGE: Episode management now requires valid task_id; max_steps are task-specific, altering default episode lengths.
- env/core.py +35 -6
- env/transitions.py +34 -14
- inference.py +49 -16
- tasks/config.py +58 -0
- tasks/registry.py +37 -0
- tasks/task1.py +56 -0
- tasks/task2.py +63 -0
- tasks/task3.py +64 -0
- tasks/task4.py +63 -0
- tests/adversarial/test_malformed_llm_outputs.py +2 -1
- tests/integration/test_inference_loop.py +4 -1
- tests/integration/test_tasks_integration.py +46 -0
- tests/unit/test_tasks.py +89 -0
- utils/constants.py +61 -62
env/core.py
CHANGED
|
@@ -12,14 +12,16 @@ from env.state import ThermalPlantState
|
|
| 12 |
from env.transitions import (
|
| 13 |
build_coherent_initial_state,
|
| 14 |
integration_step,
|
|
|
|
| 15 |
)
|
| 16 |
from utils.constants import (
|
| 17 |
ACTION_F_TARGET,
|
| 18 |
ACTION_U_TARGET,
|
| 19 |
DEFAULT_EPISODE_ID,
|
| 20 |
-
DEFAULT_MAX_STEPS,
|
| 21 |
DEFAULT_TASK_ID,
|
| 22 |
)
|
|
|
|
|
|
|
| 23 |
|
| 24 |
|
| 25 |
class ThermalPlantEnv:
|
|
@@ -39,13 +41,23 @@ class ThermalPlantEnv:
|
|
| 39 |
|
| 40 |
def __init__(
|
| 41 |
self,
|
| 42 |
-
max_steps: int =
|
| 43 |
task_id: str = DEFAULT_TASK_ID,
|
| 44 |
episode_id: int = DEFAULT_EPISODE_ID,
|
| 45 |
) -> None:
|
| 46 |
-
self.max_steps = max_steps
|
| 47 |
self.task_id = task_id
|
| 48 |
self.episode_id = int(episode_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
self._state = build_coherent_initial_state(task_id=self.task_id, episode_id=self.episode_id)
|
| 50 |
self._step_count = 0
|
| 51 |
|
|
@@ -60,12 +72,18 @@ class ThermalPlantEnv:
|
|
| 60 |
# fall back to the default task id and log a warning so validator runs
|
| 61 |
# continue (robust behaviour for external callers).
|
| 62 |
try:
|
|
|
|
|
|
|
| 63 |
self._state = build_coherent_initial_state(task_id=self.task_id, episode_id=self.episode_id)
|
| 64 |
except ValueError as exc:
|
| 65 |
logging.warning("reset(): unknown task_id '%s' - falling back to default '%s' (%s)", self.task_id, DEFAULT_TASK_ID, exc)
|
| 66 |
self.task_id = DEFAULT_TASK_ID
|
|
|
|
|
|
|
| 67 |
self._state = build_coherent_initial_state(task_id=self.task_id, episode_id=self.episode_id)
|
|
|
|
| 68 |
self._step_count = 0
|
|
|
|
| 69 |
return self._state.to_observation()
|
| 70 |
|
| 71 |
def state(self) -> Dict[str, float]:
|
|
@@ -94,6 +112,16 @@ class ThermalPlantEnv:
|
|
| 94 |
error_msg = "invalid_action_payload"
|
| 95 |
action = {}
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
next_state, reward, done_catastrophic, transition_info = integration_step(self._state, action)
|
| 98 |
|
| 99 |
if invalid_action:
|
|
@@ -102,14 +130,15 @@ class ThermalPlantEnv:
|
|
| 102 |
transition_info["error"] = error_msg
|
| 103 |
|
| 104 |
self._state = next_state
|
| 105 |
-
self._step_count += 1
|
| 106 |
|
| 107 |
-
|
|
|
|
| 108 |
|
| 109 |
info: Dict[str, Any] = {
|
| 110 |
"error": transition_info.get("error"),
|
| 111 |
"step_count": self._step_count,
|
| 112 |
"invalid_action": transition_info.get("invalid_action", False),
|
| 113 |
-
"invalid_action_penalty": transition_info.get("invalid_action_penalty", 0.0)
|
|
|
|
| 114 |
}
|
| 115 |
return self._state.to_observation(), float(reward), bool(done), info
|
|
|
|
| 12 |
from env.transitions import (
|
| 13 |
build_coherent_initial_state,
|
| 14 |
integration_step,
|
| 15 |
+
clamp_state,
|
| 16 |
)
|
| 17 |
from utils.constants import (
|
| 18 |
ACTION_F_TARGET,
|
| 19 |
ACTION_U_TARGET,
|
| 20 |
DEFAULT_EPISODE_ID,
|
|
|
|
| 21 |
DEFAULT_TASK_ID,
|
| 22 |
)
|
| 23 |
+
from tasks.config import ThermalPlantTask
|
| 24 |
+
from tasks.registry import get_task
|
| 25 |
|
| 26 |
|
| 27 |
class ThermalPlantEnv:
|
|
|
|
| 41 |
|
| 42 |
def __init__(
|
| 43 |
self,
|
| 44 |
+
max_steps: Optional[int] = None,
|
| 45 |
task_id: str = DEFAULT_TASK_ID,
|
| 46 |
episode_id: int = DEFAULT_EPISODE_ID,
|
| 47 |
) -> None:
|
|
|
|
| 48 |
self.task_id = task_id
|
| 49 |
self.episode_id = int(episode_id)
|
| 50 |
+
# Initialize task
|
| 51 |
+
try:
|
| 52 |
+
self._task: ThermalPlantTask = get_task(self.task_id)
|
| 53 |
+
self.max_steps = max_steps if max_steps is not None else self._task.max_steps
|
| 54 |
+
except ValueError as exc:
|
| 55 |
+
logging.warning("__init__(): unknown task_id '%s' - falling back to default '%s' (%s)", self.task_id, DEFAULT_TASK_ID, exc)
|
| 56 |
+
self.task_id = DEFAULT_TASK_ID
|
| 57 |
+
self._task = get_task(self.task_id)
|
| 58 |
+
self.max_steps = max_steps if max_steps is not None else self._task.max_steps
|
| 59 |
+
|
| 60 |
+
self._task.reset(self.episode_id)
|
| 61 |
self._state = build_coherent_initial_state(task_id=self.task_id, episode_id=self.episode_id)
|
| 62 |
self._step_count = 0
|
| 63 |
|
|
|
|
| 72 |
# fall back to the default task id and log a warning so validator runs
|
| 73 |
# continue (robust behaviour for external callers).
|
| 74 |
try:
|
| 75 |
+
self._task = get_task(self.task_id)
|
| 76 |
+
self.max_steps = getattr(self._task, "max_steps", self.max_steps)
|
| 77 |
self._state = build_coherent_initial_state(task_id=self.task_id, episode_id=self.episode_id)
|
| 78 |
except ValueError as exc:
|
| 79 |
logging.warning("reset(): unknown task_id '%s' - falling back to default '%s' (%s)", self.task_id, DEFAULT_TASK_ID, exc)
|
| 80 |
self.task_id = DEFAULT_TASK_ID
|
| 81 |
+
self._task = get_task(self.task_id)
|
| 82 |
+
self.max_steps = getattr(self._task, "max_steps", self.max_steps)
|
| 83 |
self._state = build_coherent_initial_state(task_id=self.task_id, episode_id=self.episode_id)
|
| 84 |
+
|
| 85 |
self._step_count = 0
|
| 86 |
+
self._task.reset(self.episode_id)
|
| 87 |
return self._state.to_observation()
|
| 88 |
|
| 89 |
def state(self) -> Dict[str, float]:
|
|
|
|
| 112 |
error_msg = "invalid_action_payload"
|
| 113 |
action = {}
|
| 114 |
|
| 115 |
+
self._step_count += 1
|
| 116 |
+
|
| 117 |
+
# Apply task disturbances before physics
|
| 118 |
+
deltas, task_event = self._task.apply_disturbance(self._state, self._step_count)
|
| 119 |
+
if deltas:
|
| 120 |
+
for k, v in deltas.items():
|
| 121 |
+
if hasattr(self._state, k):
|
| 122 |
+
setattr(self._state, k, getattr(self._state, k) + v)
|
| 123 |
+
self._state = clamp_state(self._state)
|
| 124 |
+
|
| 125 |
next_state, reward, done_catastrophic, transition_info = integration_step(self._state, action)
|
| 126 |
|
| 127 |
if invalid_action:
|
|
|
|
| 130 |
transition_info["error"] = error_msg
|
| 131 |
|
| 132 |
self._state = next_state
|
|
|
|
| 133 |
|
| 134 |
+
task_done = hasattr(self._task, "is_completed") and self._task.is_completed(self._state, self._step_count)
|
| 135 |
+
done = done_catastrophic or task_done or self._step_count >= self.max_steps
|
| 136 |
|
| 137 |
info: Dict[str, Any] = {
|
| 138 |
"error": transition_info.get("error"),
|
| 139 |
"step_count": self._step_count,
|
| 140 |
"invalid_action": transition_info.get("invalid_action", False),
|
| 141 |
+
"invalid_action_penalty": transition_info.get("invalid_action_penalty", 0.0),
|
| 142 |
+
"task_event": task_event
|
| 143 |
}
|
| 144 |
return self._state.to_observation(), float(reward), bool(done), info
|
env/transitions.py
CHANGED
|
@@ -218,7 +218,24 @@ def build_coherent_initial_state(task_id: str, episode_id: int) -> ThermalPlantS
|
|
| 218 |
S_BOUNDS[0],
|
| 219 |
S_BOUNDS[1],
|
| 220 |
)
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
control, cooling = _solve_controls(
|
| 223 |
load=load,
|
| 224 |
power=power,
|
|
@@ -295,8 +312,8 @@ def integration_step(state: ThermalPlantState, action: Dict[str, float]) -> Tupl
|
|
| 295 |
def der(x_vec):
|
| 296 |
P, T, Pr, S, D = x_vec
|
| 297 |
|
| 298 |
-
# Power:
|
| 299 |
-
dP = C.P_GAIN * next_u
|
| 300 |
|
| 301 |
# Temperature: dT/dt = a*P^1.1 - b*F^1.05 - c*(T - T_env) - e*D*T
|
| 302 |
# T_env is 0.0 effectively
|
|
@@ -372,20 +389,23 @@ def check_catastrophic(state: ThermalPlantState) -> Tuple[bool, str]:
|
|
| 372 |
|
| 373 |
|
| 374 |
def compute_reward(prev_state: ThermalPlantState, state: ThermalPlantState, delta_u: float) -> float:
|
| 375 |
-
#
|
| 376 |
-
r_track =
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
|
| 381 |
-
#
|
| 382 |
-
|
|
|
|
| 383 |
|
| 384 |
-
#
|
| 385 |
-
|
| 386 |
|
| 387 |
-
#
|
|
|
|
| 388 |
|
| 389 |
-
|
|
|
|
| 390 |
return clamp(reward, -10.0, 10.0)
|
| 391 |
|
|
|
|
| 218 |
S_BOUNDS[0],
|
| 219 |
S_BOUNDS[1],
|
| 220 |
)
|
| 221 |
+
|
| 222 |
+
# Compute gap, allowing it to vary around load for balanced control challenge
|
| 223 |
+
# The gap can be positive or negative, creating both "reduce" and "increase" scenarios
|
| 224 |
+
g_gap = regime["G_gap"]
|
| 225 |
+
|
| 226 |
+
# Clamp stress slightly if excessive to prevent artificial P reduction
|
| 227 |
+
if stress > 0.3:
|
| 228 |
+
stress = clamp(stress * 0.85, S_BOUNDS[0], S_BOUNDS[1])
|
| 229 |
+
|
| 230 |
+
power = clamp(load + g_gap - P_STRESS_DRAG * stress, P_BOUNDS[0], P_BOUNDS[1])
|
| 231 |
+
|
| 232 |
+
# Ensure observable gap: |P - L| >= 0.04 so LLM sees clear control objective
|
| 233 |
+
gap_magnitude = abs(power - load)
|
| 234 |
+
if gap_magnitude < 0.04:
|
| 235 |
+
# If gap too small, nudge power away from load in direction determined by seed
|
| 236 |
+
direction = 1.0 if (g_gap >= 0) else -1.0
|
| 237 |
+
power = clamp(load + direction * 0.08, P_BOUNDS[0], P_BOUNDS[1])
|
| 238 |
+
|
| 239 |
control, cooling = _solve_controls(
|
| 240 |
load=load,
|
| 241 |
power=power,
|
|
|
|
| 312 |
def der(x_vec):
|
| 313 |
P, T, Pr, S, D = x_vec
|
| 314 |
|
| 315 |
+
# Power: target tracking for U, minus load drag, minus degradation drag
|
| 316 |
+
dP = C.P_GAIN * (next_u - P) - C.P_LOAD_COEF * (P - state.L) - C.P_DEG_COEF * P * D
|
| 317 |
|
| 318 |
# Temperature: dT/dt = a*P^1.1 - b*F^1.05 - c*(T - T_env) - e*D*T
|
| 319 |
# T_env is 0.0 effectively
|
|
|
|
| 389 |
|
| 390 |
|
| 391 |
def compute_reward(prev_state: ThermalPlantState, state: ThermalPlantState, delta_u: float) -> float:
|
| 392 |
+
# Tracking reward: smooth linear decay as error increases
|
| 393 |
+
# At error=0: r_track=1.0, at error=0.2: r_track=0.0, at error=0.4: r_track=-1.0
|
| 394 |
+
# This avoids the cliff discontinuity and provides smooth gradient for learning
|
| 395 |
+
tracking_error = abs(state.P - state.L)
|
| 396 |
+
r_track = 1.0 - tracking_error / 0.2 # Linear decay over 0.2 error window
|
| 397 |
|
| 398 |
+
# Safety penalty: only penalize when approaching limits
|
| 399 |
+
safety_violation = max(0.0, state.T - C.SOFT_T) + max(0.0, state.Pr - C.SOFT_PR)
|
| 400 |
+
r_safety = C.SAFETY_PENALTY_COEF * safety_violation
|
| 401 |
|
| 402 |
+
# Stability penalty: penalize excessive control changes
|
| 403 |
+
r_stability = C.OSCILLATION_PENALTY_COEF * (delta_u ** 2)
|
| 404 |
|
| 405 |
+
# Stress penalty: penalize high stress accumulation
|
| 406 |
+
r_stress = C.STRESS_PENALTY_COEF * state.S if state.S > 0.1 else 0.0
|
| 407 |
|
| 408 |
+
# Combine: tracking reward + safety/stress penalty - control oscillation
|
| 409 |
+
reward = C.W_TRACK * r_track - C.W_SAFETY * r_safety - C.W_STABILITY * r_stability - 0.2 * r_stress
|
| 410 |
return clamp(reward, -10.0, 10.0)
|
| 411 |
|
inference.py
CHANGED
|
@@ -12,6 +12,7 @@ from dotenv import load_dotenv
|
|
| 12 |
load_dotenv()
|
| 13 |
|
| 14 |
from env.interface import ConcreteOpenEnvInterface
|
|
|
|
| 15 |
from utils import constants as C
|
| 16 |
from utils.logging_utils import canonical_action_string, log_end, log_start, log_step
|
| 17 |
from utils.parser import parse_llm_action
|
|
@@ -28,18 +29,36 @@ DEFAULT_ACTION = {
|
|
| 28 |
}
|
| 29 |
|
| 30 |
# These will be injected by the judge's infrastructure
|
| 31 |
-
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 32 |
-
MODEL_NAME = os.getenv("MODEL_NAME")
|
| 33 |
-
API_BASE_URL = os.getenv("API_BASE_URL")
|
| 34 |
TASK_NAME = os.getenv("THERMAL_PLANT_TASK", C.DEFAULT_TASK_ID)
|
| 35 |
EPISODE_ID = int(os.getenv("THERMAL_PLANT_EPISODE_ID", str(C.DEFAULT_EPISODE_ID)))
|
| 36 |
DEBUG = os.getenv("DEBUG", "false").lower() in ("1", "true", "yes")
|
| 37 |
|
|
|
|
| 38 |
SYSTEM_PROMPT = textwrap.dedent(
|
| 39 |
"""
|
| 40 |
-
You are controlling a thermal plant benchmark.
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
"""
|
| 44 |
).strip()
|
| 45 |
|
|
@@ -50,6 +69,7 @@ def _format_observation(observation: Dict[str, float]) -> str:
|
|
| 50 |
|
| 51 |
|
| 52 |
def build_user_prompt(
|
|
|
|
| 53 |
step: int,
|
| 54 |
observation: Dict[str, float],
|
| 55 |
last_reward: float,
|
|
@@ -59,6 +79,8 @@ def build_user_prompt(
|
|
| 59 |
history_block = "\n".join(history[-4:]) if history else "None"
|
| 60 |
return textwrap.dedent(
|
| 61 |
f"""
|
|
|
|
|
|
|
| 62 |
Step: {step}
|
| 63 |
Observation: {_format_observation(observation)}
|
| 64 |
Last reward: {last_reward:.2f}
|
|
@@ -70,6 +92,7 @@ def build_user_prompt(
|
|
| 70 |
|
| 71 |
def get_model_response(
|
| 72 |
client: OpenAI,
|
|
|
|
| 73 |
step: int,
|
| 74 |
observation: Dict[str, float],
|
| 75 |
last_reward: float,
|
|
@@ -81,7 +104,7 @@ def get_model_response(
|
|
| 81 |
model=MODEL_NAME,
|
| 82 |
messages=[
|
| 83 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 84 |
-
{"role": "user", "content": build_user_prompt(step, observation, last_reward, history)},
|
| 85 |
],
|
| 86 |
temperature=TEMPERATURE,
|
| 87 |
max_tokens=MAX_TOKENS,
|
|
@@ -93,21 +116,21 @@ def get_model_response(
|
|
| 93 |
return ""
|
| 94 |
|
| 95 |
|
| 96 |
-
def compute_normalized_score(rewards: List[float]) -> float:
|
| 97 |
"""Convert collected step rewards into a normalized score in [0, 1]."""
|
| 98 |
if not rewards:
|
| 99 |
return 0.0
|
| 100 |
positive_reward = sum(max(0.0, reward) for reward in rewards)
|
| 101 |
-
return min(max(positive_reward / float(
|
| 102 |
|
| 103 |
|
| 104 |
-
def determine_termination_reason(loop_error: Optional[str], done: bool, steps_taken: int) -> str:
|
| 105 |
"""Return a stable end-of-episode reason."""
|
| 106 |
if loop_error:
|
| 107 |
return "exception"
|
| 108 |
if done:
|
| 109 |
return "env_done"
|
| 110 |
-
if steps_taken >=
|
| 111 |
return "max_steps"
|
| 112 |
return "stopped"
|
| 113 |
|
|
@@ -115,10 +138,11 @@ def determine_termination_reason(loop_error: Optional[str], done: bool, steps_ta
|
|
| 115 |
def main() -> None:
|
| 116 |
"""Run a full deterministic inference episode and always emit end logs."""
|
| 117 |
model_name_for_logs = MODEL_NAME or "unset"
|
| 118 |
-
env = ConcreteOpenEnvInterface(
|
| 119 |
client: Optional[OpenAI] = None
|
| 120 |
last_valid_action: Optional[Dict[str, float]] = None
|
| 121 |
observation = env.reset(task_id=TASK_NAME, episode_id=EPISODE_ID)
|
|
|
|
| 122 |
history: List[str] = []
|
| 123 |
rewards: List[float] = []
|
| 124 |
score = 0.0
|
|
@@ -131,6 +155,12 @@ def main() -> None:
|
|
| 131 |
|
| 132 |
log_start(task=TASK_NAME, env=BENCHMARK, model=model_name_for_logs)
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
try:
|
| 135 |
if HF_TOKEN and MODEL_NAME and API_BASE_URL:
|
| 136 |
client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
|
|
@@ -141,11 +171,12 @@ def main() -> None:
|
|
| 141 |
flush=True,
|
| 142 |
)
|
| 143 |
|
| 144 |
-
for step in range(1,
|
| 145 |
raw_response = ""
|
| 146 |
if client is not None:
|
| 147 |
raw_response = get_model_response(
|
| 148 |
client=client,
|
|
|
|
| 149 |
step=step,
|
| 150 |
observation=observation,
|
| 151 |
last_reward=last_reward,
|
|
@@ -250,9 +281,9 @@ def main() -> None:
|
|
| 250 |
except Exception as exc:
|
| 251 |
loop_error = str(exc)
|
| 252 |
finally:
|
| 253 |
-
score = compute_normalized_score(rewards)
|
| 254 |
success = score >= SUCCESS_SCORE_THRESHOLD
|
| 255 |
-
termination_reason = determine_termination_reason(loop_error=loop_error, done=done, steps_taken=steps_taken)
|
| 256 |
trajectory.summary = TrajectorySummary(
|
| 257 |
task=TASK_NAME,
|
| 258 |
benchmark=BENCHMARK,
|
|
@@ -267,4 +298,6 @@ def main() -> None:
|
|
| 267 |
|
| 268 |
|
| 269 |
if __name__ == "__main__":
|
| 270 |
-
|
|
|
|
|
|
|
|
|
| 12 |
load_dotenv()
|
| 13 |
|
| 14 |
from env.interface import ConcreteOpenEnvInterface
|
| 15 |
+
from tasks.registry import get_task
|
| 16 |
from utils import constants as C
|
| 17 |
from utils.logging_utils import canonical_action_string, log_end, log_start, log_step
|
| 18 |
from utils.parser import parse_llm_action
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
# These will be injected by the judge's infrastructure
|
| 32 |
+
HF_TOKEN = os.getenv("HF_TOKEN") or os.getenv("API_KEY")
|
| 33 |
+
MODEL_NAME = os.getenv("MODEL_NAME", "meta-llama/Llama-3.3-70B-Instruct")
|
| 34 |
+
API_BASE_URL = os.getenv("API_BASE_URL", "https://router.huggingface.co/v1")
|
| 35 |
TASK_NAME = os.getenv("THERMAL_PLANT_TASK", C.DEFAULT_TASK_ID)
|
| 36 |
EPISODE_ID = int(os.getenv("THERMAL_PLANT_EPISODE_ID", str(C.DEFAULT_EPISODE_ID)))
|
| 37 |
DEBUG = os.getenv("DEBUG", "false").lower() in ("1", "true", "yes")
|
| 38 |
|
| 39 |
+
|
| 40 |
SYSTEM_PROMPT = textwrap.dedent(
|
| 41 |
"""
|
| 42 |
+
You are controlling a thermal plant benchmark.
|
| 43 |
+
Your objective is to track the required load (L) with your power output (P) while keeping temperature (T) and pressure (Pr) safely below 1.0 to avoid stress (S).
|
| 44 |
+
|
| 45 |
+
State Variables (Inputs):
|
| 46 |
+
P : Current Power output (track this to L)
|
| 47 |
+
L : Required Load
|
| 48 |
+
T : Temperature (keep safely below 1.0)
|
| 49 |
+
Pr: Pressure (keep safely below 1.0)
|
| 50 |
+
S : System Stress
|
| 51 |
+
D : Plant Degradation
|
| 52 |
+
U : Current Control Valve position (inertia-delayed power target)
|
| 53 |
+
F : Current Cooling Valve position (inertia-delayed cooling target)
|
| 54 |
+
|
| 55 |
+
Action Space (Your Outputs):
|
| 56 |
+
- U_target: Desired power level [0, 1]. Drives the power valve U.
|
| 57 |
+
- F_target: Desired cooling level [0, 1]. Drives the cooling valve F.
|
| 58 |
+
|
| 59 |
+
Note that your target controls (U_target, F_target) are subject to physical lag. They slowly move the actual valves (U, F) towards your targets. Anticipate delayed responses and system inertia.
|
| 60 |
+
|
| 61 |
+
Return ONLY a compact numeric pair separated by a space representing your chosen `U_target` and `F_target` (e.g. "0.60 0.50"). Do not output JSON, explanations, or prose.
|
| 62 |
"""
|
| 63 |
).strip()
|
| 64 |
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
def build_user_prompt(
|
| 72 |
+
task_description: str,
|
| 73 |
step: int,
|
| 74 |
observation: Dict[str, float],
|
| 75 |
last_reward: float,
|
|
|
|
| 79 |
history_block = "\n".join(history[-4:]) if history else "None"
|
| 80 |
return textwrap.dedent(
|
| 81 |
f"""
|
| 82 |
+
Task Objective: {task_description}
|
| 83 |
+
|
| 84 |
Step: {step}
|
| 85 |
Observation: {_format_observation(observation)}
|
| 86 |
Last reward: {last_reward:.2f}
|
|
|
|
| 92 |
|
| 93 |
def get_model_response(
|
| 94 |
client: OpenAI,
|
| 95 |
+
task_description: str,
|
| 96 |
step: int,
|
| 97 |
observation: Dict[str, float],
|
| 98 |
last_reward: float,
|
|
|
|
| 104 |
model=MODEL_NAME,
|
| 105 |
messages=[
|
| 106 |
{"role": "system", "content": SYSTEM_PROMPT},
|
| 107 |
+
{"role": "user", "content": build_user_prompt(task_description, step, observation, last_reward, history)},
|
| 108 |
],
|
| 109 |
temperature=TEMPERATURE,
|
| 110 |
max_tokens=MAX_TOKENS,
|
|
|
|
| 116 |
return ""
|
| 117 |
|
| 118 |
|
| 119 |
+
def compute_normalized_score(rewards: List[float], max_steps: int) -> float:
|
| 120 |
"""Convert collected step rewards into a normalized score in [0, 1]."""
|
| 121 |
if not rewards:
|
| 122 |
return 0.0
|
| 123 |
positive_reward = sum(max(0.0, reward) for reward in rewards)
|
| 124 |
+
return min(max(positive_reward / float(max_steps), 0.0), 1.0)
|
| 125 |
|
| 126 |
|
| 127 |
+
def determine_termination_reason(loop_error: Optional[str], done: bool, steps_taken: int, max_steps: int) -> str:
|
| 128 |
"""Return a stable end-of-episode reason."""
|
| 129 |
if loop_error:
|
| 130 |
return "exception"
|
| 131 |
if done:
|
| 132 |
return "env_done"
|
| 133 |
+
if steps_taken >= max_steps:
|
| 134 |
return "max_steps"
|
| 135 |
return "stopped"
|
| 136 |
|
|
|
|
| 138 |
def main() -> None:
|
| 139 |
"""Run a full deterministic inference episode and always emit end logs."""
|
| 140 |
model_name_for_logs = MODEL_NAME or "unset"
|
| 141 |
+
env = ConcreteOpenEnvInterface()
|
| 142 |
client: Optional[OpenAI] = None
|
| 143 |
last_valid_action: Optional[Dict[str, float]] = None
|
| 144 |
observation = env.reset(task_id=TASK_NAME, episode_id=EPISODE_ID)
|
| 145 |
+
max_steps = getattr(env._env, "max_steps", 12) if hasattr(env, "_env") else 12
|
| 146 |
history: List[str] = []
|
| 147 |
rewards: List[float] = []
|
| 148 |
score = 0.0
|
|
|
|
| 155 |
|
| 156 |
log_start(task=TASK_NAME, env=BENCHMARK, model=model_name_for_logs)
|
| 157 |
|
| 158 |
+
try:
|
| 159 |
+
task_obj = get_task(TASK_NAME)
|
| 160 |
+
task_description = getattr(task_obj, "description", "Optimal Thermal Plant Control")
|
| 161 |
+
except Exception:
|
| 162 |
+
task_description = "Optimal Thermal Plant Control"
|
| 163 |
+
|
| 164 |
try:
|
| 165 |
if HF_TOKEN and MODEL_NAME and API_BASE_URL:
|
| 166 |
client = OpenAI(base_url=API_BASE_URL, api_key=HF_TOKEN)
|
|
|
|
| 171 |
flush=True,
|
| 172 |
)
|
| 173 |
|
| 174 |
+
for step in range(1, max_steps + 1):
|
| 175 |
raw_response = ""
|
| 176 |
if client is not None:
|
| 177 |
raw_response = get_model_response(
|
| 178 |
client=client,
|
| 179 |
+
task_description=task_description,
|
| 180 |
step=step,
|
| 181 |
observation=observation,
|
| 182 |
last_reward=last_reward,
|
|
|
|
| 281 |
except Exception as exc:
|
| 282 |
loop_error = str(exc)
|
| 283 |
finally:
|
| 284 |
+
score = compute_normalized_score(rewards, max_steps)
|
| 285 |
success = score >= SUCCESS_SCORE_THRESHOLD
|
| 286 |
+
termination_reason = determine_termination_reason(loop_error=loop_error, done=done, steps_taken=steps_taken, max_steps=max_steps)
|
| 287 |
trajectory.summary = TrajectorySummary(
|
| 288 |
task=TASK_NAME,
|
| 289 |
benchmark=BENCHMARK,
|
|
|
|
| 298 |
|
| 299 |
|
| 300 |
if __name__ == "__main__":
|
| 301 |
+
if HF_TOKEN is None:
|
| 302 |
+
raise ValueError("HF_TOKEN environment variable is required")
|
| 303 |
+
main()
|
tasks/config.py
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Task configuration, interfaces, and shared types."""
|
| 2 |
+
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
from typing import Dict, Optional, Tuple, Any
|
| 5 |
+
|
| 6 |
+
from env.state import ThermalPlantState
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class AgentPolicy(ABC):
|
| 10 |
+
"""A deterministic baseline controller for sanity checks and grading."""
|
| 11 |
+
|
| 12 |
+
@abstractmethod
|
| 13 |
+
def get_action(self, observation: Dict[str, float]) -> Dict[str, float]:
|
| 14 |
+
"""Return a deterministic control action based on the state observation."""
|
| 15 |
+
pass
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ThermalPlantTask(ABC):
|
| 19 |
+
"""Base class for tasks governing the episodes in the standard benchmark.
|
| 20 |
+
|
| 21 |
+
Tasks control deterministic disturbances over time and provide metadata
|
| 22 |
+
for graders and OpenEnv integration.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
task_id: str
|
| 26 |
+
name: str
|
| 27 |
+
description: str
|
| 28 |
+
max_steps: int
|
| 29 |
+
|
| 30 |
+
@abstractmethod
|
| 31 |
+
def reset(self, episode_id: int) -> None:
|
| 32 |
+
"""Reset the task-internal state using the episode ID as a deterministic seed."""
|
| 33 |
+
pass
|
| 34 |
+
|
| 35 |
+
@abstractmethod
|
| 36 |
+
def apply_disturbance(self, state: ThermalPlantState, step: int) -> Tuple[Dict[str, float], Optional[Dict[str, Any]]]:
|
| 37 |
+
"""Compute relative disturbances for the current step.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
state: The fully specified current actual state (un-rounded).
|
| 41 |
+
step: The current 1-indexed environment step.
|
| 42 |
+
|
| 43 |
+
Returns:
|
| 44 |
+
deltas: A dictionary mapping state keys (e.g., 'L', 'D', 'T') to absolute Delta values
|
| 45 |
+
to add to the current state fields. Amplitudes should be derived relative
|
| 46 |
+
to the variable domain bounds implicitly.
|
| 47 |
+
event: An optional dictionary describing the event that just occurred for info logging.
|
| 48 |
+
"""
|
| 49 |
+
pass
|
| 50 |
+
|
| 51 |
+
def is_completed(self, state: ThermalPlantState, step_count: int) -> bool:
|
| 52 |
+
"""Check if the task has successfully reached a terminal stabilizing state."""
|
| 53 |
+
return False
|
| 54 |
+
|
| 55 |
+
@abstractmethod
|
| 56 |
+
def get_baseline_policy(self) -> AgentPolicy:
|
| 57 |
+
"""Return a simple heuristic or rule-based agent capable of surviving the task."""
|
| 58 |
+
pass
|
tasks/registry.py
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Central task discovery and instantiation."""
|
| 2 |
+
|
| 3 |
+
from typing import Dict, Type, Optional
|
| 4 |
+
|
| 5 |
+
from tasks.config import ThermalPlantTask
|
| 6 |
+
from tasks.task1 import Task1
|
| 7 |
+
from tasks.task2 import Task2
|
| 8 |
+
from tasks.task3 import Task3
|
| 9 |
+
from tasks.task4 import Task4
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
_TASK_CLASSES: Dict[str, Type[ThermalPlantTask]] = {
|
| 13 |
+
"task1": Task1,
|
| 14 |
+
"task2": Task2,
|
| 15 |
+
"task3": Task3,
|
| 16 |
+
"task4": Task4,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def get_task(task_id: str) -> ThermalPlantTask:
|
| 21 |
+
"""Instantiate and return the canonical task."""
|
| 22 |
+
cls = _TASK_CLASSES.get(task_id)
|
| 23 |
+
if not cls:
|
| 24 |
+
raise ValueError(f"Unknown task_id '{task_id}'. Expected one of {list(_TASK_CLASSES.keys())}")
|
| 25 |
+
return cls()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def task_registry() -> Dict[str, dict]:
|
| 29 |
+
"""Expose registry metadata required by OpenEnv validation."""
|
| 30 |
+
return {
|
| 31 |
+
task_id: {
|
| 32 |
+
"name": getattr(cls, "name", task_id),
|
| 33 |
+
"description": getattr(cls, "description", ""),
|
| 34 |
+
"max_steps": getattr(cls, "max_steps", None),
|
| 35 |
+
}
|
| 36 |
+
for task_id, cls in _TASK_CLASSES.items()
|
| 37 |
+
}
|
tasks/task1.py
CHANGED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Task 1: Stable Baseline Operation.
|
| 2 |
+
|
| 3 |
+
Maintain P close to L while staying safe and smooth. Constant load pattern L_t = 0.6.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Dict, Optional, Tuple, Any
|
| 7 |
+
|
| 8 |
+
from env.state import ThermalPlantState
|
| 9 |
+
from tasks.config import ThermalPlantTask, AgentPolicy
|
| 10 |
+
from utils.constants import U_BOUNDS, F_BOUNDS, P_BOUNDS, L_BOUNDS, T_BOUNDS, PR_BOUNDS
|
| 11 |
+
|
| 12 |
+
class BaselinePolicy(AgentPolicy):
|
| 13 |
+
"""A simple rule-based tracker."""
|
| 14 |
+
|
| 15 |
+
def get_action(self, observation: Dict[str, float]) -> Dict[str, float]:
|
| 16 |
+
# Track power to load
|
| 17 |
+
u_target = 0.6
|
| 18 |
+
if observation["P"] < observation["L"] - 0.05:
|
| 19 |
+
u_target = min(0.9, observation["U"] + 0.1)
|
| 20 |
+
elif observation["P"] > observation["L"] + 0.05:
|
| 21 |
+
u_target = max(0.1, observation["U"] - 0.1)
|
| 22 |
+
|
| 23 |
+
# Heavy cooling if temp/pressure gets hot
|
| 24 |
+
f_target = 0.4
|
| 25 |
+
if observation["T"] > 0.85 or observation["Pr"] > 0.85:
|
| 26 |
+
f_target = 0.8
|
| 27 |
+
u_target = max(0.1, u_target - 0.2)
|
| 28 |
+
|
| 29 |
+
return {"U_target": u_target, "F_target": f_target}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class Task1(ThermalPlantTask):
|
| 33 |
+
task_id = "task1"
|
| 34 |
+
name = "Stable Baseline Operation"
|
| 35 |
+
description = "Maintain power close to a constant load of 0.6 while staying safe and smooth."
|
| 36 |
+
max_steps = 12
|
| 37 |
+
|
| 38 |
+
def reset(self, episode_id: int) -> None:
|
| 39 |
+
self._seed = episode_id
|
| 40 |
+
|
| 41 |
+
def apply_disturbance(self, state: ThermalPlantState, step: int) -> Tuple[Dict[str, float], Optional[Dict[str, Any]]]:
|
| 42 |
+
deltas = {}
|
| 43 |
+
# Force L_t = 0.6
|
| 44 |
+
target_L = 0.6
|
| 45 |
+
if abs(state.L - target_L) > 1e-5:
|
| 46 |
+
deltas["L"] = target_L - state.L
|
| 47 |
+
return deltas, {"type": "constant_load", "target_L": target_L}
|
| 48 |
+
|
| 49 |
+
def is_completed(self, state: ThermalPlantState, step_count: int) -> bool:
|
| 50 |
+
# User requested early stop when P=L.
|
| 51 |
+
tracking_error = abs(state.P - 0.6)
|
| 52 |
+
# End task immediately if error is very small (converged to load target)
|
| 53 |
+
return tracking_error <= 0.02 and step_count >= 3
|
| 54 |
+
|
| 55 |
+
def get_baseline_policy(self) -> AgentPolicy:
|
| 56 |
+
return BaselinePolicy()
|
tasks/task2.py
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Task 2: Load Following.
|
| 2 |
+
|
| 3 |
+
Track changing load with minimal lag and overshoot.
|
| 4 |
+
Step changes: t=1..3: L=0.5, t=4..6: L=0.8, t=7..N: L=0.6.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from typing import Dict, Optional, Tuple, Any
|
| 8 |
+
|
| 9 |
+
from env.state import ThermalPlantState
|
| 10 |
+
from tasks.config import ThermalPlantTask, AgentPolicy
|
| 11 |
+
from utils.constants import L_BOUNDS, D_BOUNDS, TASK_CODE
|
| 12 |
+
|
| 13 |
+
class PeriodicPolicy(AgentPolicy):
|
| 14 |
+
"""Tracker robust to periodic disturbances."""
|
| 15 |
+
|
| 16 |
+
def get_action(self, observation: Dict[str, float]) -> Dict[str, float]:
|
| 17 |
+
# Track power to load aggressively to minimize lag
|
| 18 |
+
u_target = min(max(observation["U"] + 0.4 * (observation["L"] - observation["P"]), 0.1), 0.9)
|
| 19 |
+
f_target = 0.5
|
| 20 |
+
if observation["T"] > 0.8:
|
| 21 |
+
f_target = 0.75
|
| 22 |
+
u_target -= 0.15
|
| 23 |
+
elif observation["T"] > 0.9:
|
| 24 |
+
f_target = 0.95
|
| 25 |
+
u_target = 0.1
|
| 26 |
+
|
| 27 |
+
return {"U_target": max(0.0, u_target), "F_target": f_target}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class Task2(ThermalPlantTask):
|
| 31 |
+
task_id = "task2"
|
| 32 |
+
name = "Load Following"
|
| 33 |
+
description = "Track step changes in required load (0.5 -> 0.8 -> 0.6) with minimal lag and overshoot."
|
| 34 |
+
max_steps = 12
|
| 35 |
+
|
| 36 |
+
def reset(self, episode_id: int) -> None:
|
| 37 |
+
self._seed = episode_id
|
| 38 |
+
|
| 39 |
+
def apply_disturbance(self, state: ThermalPlantState, step: int) -> Tuple[Dict[str, float], Optional[Dict[str, Any]]]:
|
| 40 |
+
deltas = {}
|
| 41 |
+
|
| 42 |
+
if step <= 3:
|
| 43 |
+
target_L = 0.5
|
| 44 |
+
elif step <= 6:
|
| 45 |
+
target_L = 0.8
|
| 46 |
+
else:
|
| 47 |
+
target_L = 0.6
|
| 48 |
+
|
| 49 |
+
if abs(state.L - target_L) > 1e-5:
|
| 50 |
+
deltas["L"] = target_L - state.L
|
| 51 |
+
|
| 52 |
+
event = {"type": "load_step", "target_L": target_L}
|
| 53 |
+
return deltas, event
|
| 54 |
+
|
| 55 |
+
def is_completed(self, state: ThermalPlantState, step_count: int) -> bool:
|
| 56 |
+
# Task 2 has a final load step change at step 7.
|
| 57 |
+
# So we only allow early completion after the final change has stabilized.
|
| 58 |
+
if step_count < 8:
|
| 59 |
+
return False
|
| 60 |
+
return abs(state.P - 0.6) <= 0.02
|
| 61 |
+
|
| 62 |
+
def get_baseline_policy(self) -> AgentPolicy:
|
| 63 |
+
return PeriodicPolicy()
|
tasks/task3.py
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Task 3: Preemptive Constraint Management.
|
| 2 |
+
|
| 3 |
+
L_t = moderately high constant (0.7).
|
| 4 |
+
Stress accumulates when T > 0.9. Agent must reduce risk BEFORE visible failure.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from typing import Dict, Optional, Tuple, Any
|
| 8 |
+
|
| 9 |
+
from env.state import ThermalPlantState
|
| 10 |
+
from tasks.config import ThermalPlantTask, AgentPolicy
|
| 11 |
+
from utils.constants import L_BOUNDS, TASK_CODE
|
| 12 |
+
|
| 13 |
+
class RampPolicy(AgentPolicy):
|
| 14 |
+
"""Proportional tracker."""
|
| 15 |
+
|
| 16 |
+
def get_action(self, observation: Dict[str, float]) -> Dict[str, float]:
|
| 17 |
+
# Track power to load proportionally
|
| 18 |
+
u_target = min(observation["U"] + 0.4 * (observation["L"] - observation["P"]), 1.0)
|
| 19 |
+
u_target = max(u_target, 0.0)
|
| 20 |
+
|
| 21 |
+
f_target = max(0.4, 0.8 * observation["T"])
|
| 22 |
+
# Preemptively keep T < 0.9 by sharply increasing cooling when safely near the threshold
|
| 23 |
+
if observation["T"] > 0.85:
|
| 24 |
+
f_target = 0.95
|
| 25 |
+
u_target = max(0.0, u_target - 0.2)
|
| 26 |
+
|
| 27 |
+
return {"U_target": u_target, "F_target": f_target}
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class Task3(ThermalPlantTask):
|
| 31 |
+
task_id = "task3"
|
| 32 |
+
name = "Preemptive Constraint Management"
|
| 33 |
+
description = "Manage moderately high load (0.7) and prevent stress accumulation by keeping Temperature below 0.9 under restricted coolant conditions."
|
| 34 |
+
max_steps = 12
|
| 35 |
+
|
| 36 |
+
def reset(self, episode_id: int) -> None:
|
| 37 |
+
self._seed = int(episode_id)
|
| 38 |
+
|
| 39 |
+
def apply_disturbance(self, state: ThermalPlantState, step: int) -> Tuple[Dict[str, float], Optional[Dict[str, Any]]]:
|
| 40 |
+
deltas: Dict[str, float] = {}
|
| 41 |
+
event: Dict[str, Any] = {"type": "constraint_management"}
|
| 42 |
+
|
| 43 |
+
target_L = 0.7
|
| 44 |
+
if abs(state.L - target_L) > 1e-5:
|
| 45 |
+
deltas["L"] = target_L - state.L
|
| 46 |
+
|
| 47 |
+
# Exogenous heat: acts as a coolant deficiency.
|
| 48 |
+
# Max cooling (F=1.0) is no longer strong enough to overcome both this and P=0.7.
|
| 49 |
+
deltas["T"] = 0.045
|
| 50 |
+
|
| 51 |
+
# Stress accumulates when T > 0.9
|
| 52 |
+
if state.T > 0.9:
|
| 53 |
+
deltas["S"] = 0.05 # Additive stress accumulation per step
|
| 54 |
+
event["stress_warning"] = True
|
| 55 |
+
|
| 56 |
+
return deltas, event
|
| 57 |
+
|
| 58 |
+
def is_completed(self, state: ThermalPlantState, step_count: int) -> bool:
|
| 59 |
+
# End task if stable and constraints are well managed.
|
| 60 |
+
error = abs(state.P - 0.7)
|
| 61 |
+
return error <= 0.02 and state.T < 0.85 and step_count >= 3
|
| 62 |
+
|
| 63 |
+
def get_baseline_policy(self) -> AgentPolicy:
|
| 64 |
+
return RampPolicy()
|
tasks/task4.py
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Task 4: Fault Recovery with Degradation.
|
| 2 |
+
|
| 3 |
+
L=0.6 mostly. Disturbance at t=4: T spike (+0.3).
|
| 4 |
+
Degradation reduces cooling effectiveness over time.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from typing import Dict, Optional, Tuple, Any
|
| 8 |
+
|
| 9 |
+
from env.state import ThermalPlantState
|
| 10 |
+
from tasks.config import ThermalPlantTask, AgentPolicy
|
| 11 |
+
from utils.constants import T_BOUNDS, PR_BOUNDS, TASK_CODE
|
| 12 |
+
|
| 13 |
+
class ShockPolicy(AgentPolicy):
|
| 14 |
+
"""Heavy cooling strategy."""
|
| 15 |
+
|
| 16 |
+
def get_action(self, observation: Dict[str, float]) -> Dict[str, float]:
|
| 17 |
+
# Track power to load cautiously
|
| 18 |
+
u_target = min(observation["U"] + 0.2 * (observation["L"] - observation["P"]), 0.8)
|
| 19 |
+
|
| 20 |
+
# Heavy cooling
|
| 21 |
+
f_target = 0.6
|
| 22 |
+
if observation["T"] > 0.8 or observation["Pr"] > 0.8:
|
| 23 |
+
f_target = 1.0
|
| 24 |
+
u_target = 0.0 # Emergency shutdown
|
| 25 |
+
|
| 26 |
+
return {"U_target": max(0.0, u_target), "F_target": f_target}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class Task4(ThermalPlantTask):
|
| 30 |
+
task_id = "task4"
|
| 31 |
+
name = "Fault Recovery with Degradation"
|
| 32 |
+
description = "Recover from a major thermal spike at step 4 while managing ongoing system degradation."
|
| 33 |
+
max_steps = 12
|
| 34 |
+
|
| 35 |
+
def reset(self, episode_id: int) -> None:
|
| 36 |
+
self._seed = int(episode_id)
|
| 37 |
+
|
| 38 |
+
def apply_disturbance(self, state: ThermalPlantState, step: int) -> Tuple[Dict[str, float], Optional[Dict[str, Any]]]:
|
| 39 |
+
deltas = {}
|
| 40 |
+
event = {"type": "constant_load"}
|
| 41 |
+
|
| 42 |
+
target_L = 0.6
|
| 43 |
+
if abs(state.L - target_L) > 1e-5:
|
| 44 |
+
deltas["L"] = target_L - state.L
|
| 45 |
+
|
| 46 |
+
if step == 4:
|
| 47 |
+
deltas["T"] = 0.5
|
| 48 |
+
event = {"type": "thermal_fault", "T_delta": deltas["T"]}
|
| 49 |
+
|
| 50 |
+
# Ongoing degradation to simulate reduced cooling effectiveness
|
| 51 |
+
deltas["D"] = 0.02
|
| 52 |
+
|
| 53 |
+
return deltas, event
|
| 54 |
+
|
| 55 |
+
def is_completed(self, state: ThermalPlantState, step_count: int) -> bool:
|
| 56 |
+
# Task 4 has a major fault at step 4.
|
| 57 |
+
# Don't end early before the fault has been processed and stabilized.
|
| 58 |
+
if step_count < 6:
|
| 59 |
+
return False
|
| 60 |
+
return abs(state.P - 0.6) <= 0.02 and state.T < 0.8 and state.Pr < 0.8
|
| 61 |
+
|
| 62 |
+
def get_baseline_policy(self) -> AgentPolicy:
|
| 63 |
+
return ShockPolicy()
|
tests/adversarial/test_malformed_llm_outputs.py
CHANGED
|
@@ -20,7 +20,8 @@ def mock_env_vars():
|
|
| 20 |
|
| 21 |
def test_inference_main_with_malformed_outputs(capsys, monkeypatch, mock_env_vars):
|
| 22 |
# Mock settings to create a short run
|
| 23 |
-
|
|
|
|
| 24 |
monkeypatch.setattr(inference.C, "INCLUDE_PARSE_ERROR_IN_STEP", True)
|
| 25 |
|
| 26 |
# We will simulate exactly 4 steps of LLM responses:
|
|
|
|
| 20 |
|
| 21 |
def test_inference_main_with_malformed_outputs(capsys, monkeypatch, mock_env_vars):
|
| 22 |
# Mock settings to create a short run
|
| 23 |
+
import tasks.task2
|
| 24 |
+
monkeypatch.setattr(tasks.task2.Task2, "max_steps", 4)
|
| 25 |
monkeypatch.setattr(inference.C, "INCLUDE_PARSE_ERROR_IN_STEP", True)
|
| 26 |
|
| 27 |
# We will simulate exactly 4 steps of LLM responses:
|
tests/integration/test_inference_loop.py
CHANGED
|
@@ -20,6 +20,7 @@ MOCK_RESPONSES = [
|
|
| 20 |
|
| 21 |
def mock_get_model_response(
|
| 22 |
client: MagicMock,
|
|
|
|
| 23 |
step: int,
|
| 24 |
observation: Dict[str, float],
|
| 25 |
last_reward: float,
|
|
@@ -35,7 +36,9 @@ def mock_inference_env(monkeypatch):
|
|
| 35 |
monkeypatch.setenv("MODEL_NAME", "fake_model")
|
| 36 |
monkeypatch.setenv("API_BASE_URL", "http://fake.api")
|
| 37 |
monkeypatch.setenv("THERMAL_PLANT_EPISODE_ID", "1")
|
| 38 |
-
|
|
|
|
|
|
|
| 39 |
yield
|
| 40 |
|
| 41 |
def test_inference_main_prints_exact_stdout_format(mock_inference_env, monkeypatch):
|
|
|
|
| 20 |
|
| 21 |
def mock_get_model_response(
|
| 22 |
client: MagicMock,
|
| 23 |
+
task_description: str,
|
| 24 |
step: int,
|
| 25 |
observation: Dict[str, float],
|
| 26 |
last_reward: float,
|
|
|
|
| 36 |
monkeypatch.setenv("MODEL_NAME", "fake_model")
|
| 37 |
monkeypatch.setenv("API_BASE_URL", "http://fake.api")
|
| 38 |
monkeypatch.setenv("THERMAL_PLANT_EPISODE_ID", "1")
|
| 39 |
+
# Tell the default task it's a short episode
|
| 40 |
+
import tasks.task2
|
| 41 |
+
monkeypatch.setattr(tasks.task2.Task2, "max_steps", 5)
|
| 42 |
yield
|
| 43 |
|
| 44 |
def test_inference_main_prints_exact_stdout_format(mock_inference_env, monkeypatch):
|
tests/integration/test_tasks_integration.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
import io
|
| 3 |
+
import contextlib
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
import inference
|
| 7 |
+
from env.core import ThermalPlantEnv
|
| 8 |
+
from utils.constants import TASK_CODE
|
| 9 |
+
|
| 10 |
+
@pytest.fixture
|
| 11 |
+
def mock_inference_env_tasks(monkeypatch):
|
| 12 |
+
monkeypatch.setenv("HF_TOKEN", "fake_token")
|
| 13 |
+
monkeypatch.setenv("MODEL_NAME", "fake_model")
|
| 14 |
+
monkeypatch.setenv("API_BASE_URL", "http://fake.api")
|
| 15 |
+
monkeypatch.setenv("THERMAL_PLANT_EPISODE_ID", "42")
|
| 16 |
+
yield monkeypatch
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@pytest.mark.parametrize("task_id", ["task1", "task2", "task3", "task4"])
|
| 20 |
+
def test_inference_runs_full_episode_for_each_task(mock_inference_env_tasks, task_id):
|
| 21 |
+
mock_inference_env_tasks.setenv("THERMAL_PLANT_TASK", task_id)
|
| 22 |
+
|
| 23 |
+
# We will hook inference so it just runs the task's baseline policy instead of calling a model
|
| 24 |
+
env = ThermalPlantEnv(task_id=task_id, episode_id=42)
|
| 25 |
+
policy = env._task.get_baseline_policy()
|
| 26 |
+
|
| 27 |
+
def mock_get_model_response(client, task_description, step, observation, last_reward, history):
|
| 28 |
+
action = policy.get_action(observation)
|
| 29 |
+
# convert back to valid string
|
| 30 |
+
return f"{action['U_target']:.2f} {action['F_target']:.2f}"
|
| 31 |
+
|
| 32 |
+
mock_inference_env_tasks.setattr(inference, "get_model_response", mock_get_model_response)
|
| 33 |
+
|
| 34 |
+
stdout_capture = io.StringIO()
|
| 35 |
+
with contextlib.redirect_stdout(stdout_capture):
|
| 36 |
+
inference.main()
|
| 37 |
+
|
| 38 |
+
output = stdout_capture.getvalue()
|
| 39 |
+
lines = output.strip().split("\n")
|
| 40 |
+
|
| 41 |
+
assert lines[0].startswith(f"[START] task={task_id} env=thermal-plant-control")
|
| 42 |
+
assert lines[-1].startswith("[END] success=")
|
| 43 |
+
|
| 44 |
+
step_lines = [line for line in lines if line.startswith("[STEP]")]
|
| 45 |
+
max_steps = env.max_steps
|
| 46 |
+
assert len(step_lines) <= max_steps
|
tests/unit/test_tasks.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
|
| 3 |
+
from env.core import ThermalPlantEnv
|
| 4 |
+
from tasks.registry import get_task
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@pytest.mark.parametrize("task_id", ["task1", "task2", "task3", "task4"])
|
| 8 |
+
def test_task_registry_has_tasks(task_id):
|
| 9 |
+
task = get_task(task_id)
|
| 10 |
+
assert task.task_id == task_id
|
| 11 |
+
assert task.max_steps > 0
|
| 12 |
+
assert task.get_baseline_policy() is not None
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_task1_baseline():
|
| 16 |
+
env = ThermalPlantEnv(task_id="task1", episode_id=42)
|
| 17 |
+
obs = env.reset()
|
| 18 |
+
assert env.max_steps == 12
|
| 19 |
+
|
| 20 |
+
# Task1 applies NO disturbances except constant Load enforcement
|
| 21 |
+
action = env._task.get_baseline_policy().get_action(obs)
|
| 22 |
+
obs, reward, done, info = env.step(action)
|
| 23 |
+
|
| 24 |
+
assert info.get("task_event") is not None
|
| 25 |
+
assert info["task_event"]["type"] == "constant_load"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_task2_periodic_pulses():
|
| 29 |
+
env = ThermalPlantEnv(task_id="task2", episode_id=42)
|
| 30 |
+
obs = env.reset()
|
| 31 |
+
assert env.max_steps == 12
|
| 32 |
+
|
| 33 |
+
events_seen = 0
|
| 34 |
+
for step in range(1, 13):
|
| 35 |
+
action = env._task.get_baseline_policy().get_action(obs)
|
| 36 |
+
obs, reward, done, info = env.step(action)
|
| 37 |
+
if info.get("task_event"):
|
| 38 |
+
assert info["task_event"]["type"] == "load_step"
|
| 39 |
+
target = info["task_event"]["target_L"]
|
| 40 |
+
if step <= 3:
|
| 41 |
+
assert target == 0.5
|
| 42 |
+
elif step <= 6:
|
| 43 |
+
assert target == 0.8
|
| 44 |
+
else:
|
| 45 |
+
assert target == 0.6
|
| 46 |
+
|
| 47 |
+
if done:
|
| 48 |
+
break
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def test_task3_ramp():
|
| 52 |
+
env = ThermalPlantEnv(task_id="task3", episode_id=42)
|
| 53 |
+
obs = env.reset()
|
| 54 |
+
assert env.max_steps == 12
|
| 55 |
+
|
| 56 |
+
events_seen = 0
|
| 57 |
+
# Artificially spike T to test stress accumulation
|
| 58 |
+
env._state.T = 0.95
|
| 59 |
+
for _ in range(12):
|
| 60 |
+
action = env._task.get_baseline_policy().get_action(obs)
|
| 61 |
+
obs, reward, done, info = env.step(action)
|
| 62 |
+
if info.get("task_event") is not None:
|
| 63 |
+
events_seen += 1
|
| 64 |
+
|
| 65 |
+
if done:
|
| 66 |
+
break
|
| 67 |
+
|
| 68 |
+
assert events_seen > 0, "No events occurred in task3"
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_task4_thermal_shock():
|
| 72 |
+
env = ThermalPlantEnv(task_id="task4", episode_id=42)
|
| 73 |
+
obs = env.reset()
|
| 74 |
+
assert env.max_steps == 12
|
| 75 |
+
|
| 76 |
+
events_seen = 0
|
| 77 |
+
for step in range(1, 13):
|
| 78 |
+
action = env._task.get_baseline_policy().get_action(obs)
|
| 79 |
+
obs, reward, done, info = env.step(action)
|
| 80 |
+
if info.get("task_event") is not None:
|
| 81 |
+
if info["task_event"].get("type") == "thermal_fault":
|
| 82 |
+
events_seen += 1
|
| 83 |
+
assert step == 4
|
| 84 |
+
|
| 85 |
+
if done:
|
| 86 |
+
break
|
| 87 |
+
|
| 88 |
+
# Should see exactly 1 thermal shock at step 4
|
| 89 |
+
assert events_seen == 1, f"Expected 1 thermal shock, got {events_seen}"
|
utils/constants.py
CHANGED
|
@@ -47,16 +47,15 @@ DEFAULT_STATE: Dict[str, float] = {
|
|
| 47 |
}
|
| 48 |
|
| 49 |
# Episode controls.
|
| 50 |
-
#
|
| 51 |
-
DEFAULT_MAX_STEPS = 12
|
| 52 |
DEFAULT_TASK_ID = "task1"
|
| 53 |
-
DEFAULT_EPISODE_ID =
|
| 54 |
OBSERVATION_DECIMALS = 2
|
| 55 |
|
| 56 |
# External evaluator episode policy
|
| 57 |
# External (evaluator/public) calls will use this fixed episode id unless
|
| 58 |
# a valid developer token is supplied via the `X-DEV-TOKEN` header.
|
| 59 |
-
DEFAULT_EXTERNAL_EPISODE_ID =
|
| 60 |
EXTERNAL_EPISODE_DISPLAY_WIDTH = 3
|
| 61 |
|
| 62 |
# Name of the environment variable that holds the developer reset token.
|
|
@@ -110,75 +109,75 @@ INIT_T_F_GAIN = 0.30
|
|
| 110 |
TASK_STARTUP_PROFILES: Dict[str, Dict[str, float]] = {
|
| 111 |
"task1": {
|
| 112 |
"aL": 0.60,
|
| 113 |
-
"sL": 0.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
"aD": 0.04,
|
| 115 |
-
"sD": 0.
|
| 116 |
-
"aT": 0.
|
| 117 |
"sT": 0.10,
|
| 118 |
-
"aM": 0.
|
| 119 |
"sM": 0.08,
|
| 120 |
-
"gap_scale": 0.
|
| 121 |
-
"d_max": 0.15,
|
| 122 |
-
"s_base": 0.10,
|
| 123 |
-
"s_gain": 0.14,
|
| 124 |
-
"f_bias": 0.16,
|
| 125 |
-
"t_task_bias": -0.06,
|
| 126 |
-
"soft_t_cap": 0.78,
|
| 127 |
-
"soft_pr_cap": 0.84,
|
| 128 |
-
},
|
| 129 |
-
"task2": {
|
| 130 |
-
"aL": 0.50,
|
| 131 |
-
"sL": 0.12,
|
| 132 |
-
"aD": 0.05,
|
| 133 |
-
"sD": 0.06,
|
| 134 |
-
"aT": 0.18,
|
| 135 |
-
"sT": 0.14,
|
| 136 |
-
"aM": 0.70,
|
| 137 |
-
"sM": 0.10,
|
| 138 |
-
"gap_scale": 0.08,
|
| 139 |
"d_max": 0.10,
|
| 140 |
-
"s_base": 0.
|
| 141 |
-
"s_gain": 0.
|
| 142 |
-
"f_bias": 0.
|
| 143 |
-
"t_task_bias": -0.
|
| 144 |
-
"soft_t_cap": 0.
|
| 145 |
"soft_pr_cap": 0.88,
|
| 146 |
},
|
| 147 |
"task3": {
|
| 148 |
"aL": 0.70,
|
| 149 |
-
"sL": 0.
|
| 150 |
-
"aD": 0.
|
| 151 |
-
"sD": 0.
|
| 152 |
-
"aT": 0.
|
| 153 |
-
"sT": 0.
|
| 154 |
-
"aM": 0.
|
| 155 |
-
"sM": 0.
|
| 156 |
-
"gap_scale": 0.
|
| 157 |
-
"d_max": 0.
|
| 158 |
-
"s_base": 0.
|
| 159 |
-
"s_gain": 0.
|
| 160 |
-
"f_bias": 0.
|
| 161 |
-
"t_task_bias": 0.
|
| 162 |
-
"soft_t_cap":
|
| 163 |
-
"soft_pr_cap":
|
| 164 |
},
|
| 165 |
"task4": {
|
| 166 |
"aL": 0.60,
|
| 167 |
-
"sL": 0.
|
| 168 |
-
"aD": 0.
|
| 169 |
-
"sD": 0.
|
| 170 |
-
"aT": 0.
|
| 171 |
-
"sT": 0.
|
| 172 |
-
"aM": 0.
|
| 173 |
-
"sM": 0.
|
| 174 |
-
"gap_scale": 0.
|
| 175 |
-
"d_max": 0.
|
| 176 |
-
"s_base": 0.
|
| 177 |
-
"s_gain": 0.
|
| 178 |
-
"f_bias": 0.
|
| 179 |
-
"t_task_bias": 0.
|
| 180 |
-
"soft_t_cap": 0.
|
| 181 |
-
"soft_pr_cap": 0.
|
| 182 |
},
|
| 183 |
}
|
| 184 |
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
# Episode controls.
|
| 50 |
+
# Tasks dictate default episode lengths. Max steps are defined in the specific Task subclasses.
|
|
|
|
| 51 |
DEFAULT_TASK_ID = "task1"
|
| 52 |
+
DEFAULT_EPISODE_ID = 143
|
| 53 |
OBSERVATION_DECIMALS = 2
|
| 54 |
|
| 55 |
# External evaluator episode policy
|
| 56 |
# External (evaluator/public) calls will use this fixed episode id unless
|
| 57 |
# a valid developer token is supplied via the `X-DEV-TOKEN` header.
|
| 58 |
+
DEFAULT_EXTERNAL_EPISODE_ID = 143
|
| 59 |
EXTERNAL_EPISODE_DISPLAY_WIDTH = 3
|
| 60 |
|
| 61 |
# Name of the environment variable that holds the developer reset token.
|
|
|
|
| 109 |
TASK_STARTUP_PROFILES: Dict[str, Dict[str, float]] = {
|
| 110 |
"task1": {
|
| 111 |
"aL": 0.60,
|
| 112 |
+
"sL": 0.00, # Fixed load start to match constant task target
|
| 113 |
+
"aD": 0.02,
|
| 114 |
+
"sD": 0.03,
|
| 115 |
+
"aT": 0.08,
|
| 116 |
+
"sT": 0.06,
|
| 117 |
+
"aM": 0.88,
|
| 118 |
+
"sM": 0.06,
|
| 119 |
+
"gap_scale": 0.22,
|
| 120 |
+
"d_max": 0.08,
|
| 121 |
+
"s_base": 0.05,
|
| 122 |
+
"s_gain": 0.08,
|
| 123 |
+
"f_bias": 0.20,
|
| 124 |
+
"t_task_bias": -0.08,
|
| 125 |
+
"soft_t_cap": 0.82,
|
| 126 |
+
"soft_pr_cap": 0.86,
|
| 127 |
+
},
|
| 128 |
+
"task2": {
|
| 129 |
+
"aL": 0.50, # Set to exact step 1 load sequence baseline
|
| 130 |
+
"sL": 0.00,
|
| 131 |
"aD": 0.04,
|
| 132 |
+
"sD": 0.04,
|
| 133 |
+
"aT": 0.14,
|
| 134 |
"sT": 0.10,
|
| 135 |
+
"aM": 0.78,
|
| 136 |
"sM": 0.08,
|
| 137 |
+
"gap_scale": 0.16,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
"d_max": 0.10,
|
| 139 |
+
"s_base": 0.03,
|
| 140 |
+
"s_gain": 0.12,
|
| 141 |
+
"f_bias": 0.22,
|
| 142 |
+
"t_task_bias": -0.04,
|
| 143 |
+
"soft_t_cap": 0.84,
|
| 144 |
"soft_pr_cap": 0.88,
|
| 145 |
},
|
| 146 |
"task3": {
|
| 147 |
"aL": 0.70,
|
| 148 |
+
"sL": 0.00,
|
| 149 |
+
"aD": 0.08,
|
| 150 |
+
"sD": 0.06,
|
| 151 |
+
"aT": 0.72,
|
| 152 |
+
"sT": 0.12,
|
| 153 |
+
"aM": 0.60,
|
| 154 |
+
"sM": 0.08,
|
| 155 |
+
"gap_scale": 0.05, # Reduce gap further, so power is already high and generating heat
|
| 156 |
+
"d_max": 0.18,
|
| 157 |
+
"s_base": 0.20,
|
| 158 |
+
"s_gain": 0.18,
|
| 159 |
+
"f_bias": 0.12,
|
| 160 |
+
"t_task_bias": 0.35, # Extra direct heat
|
| 161 |
+
"soft_t_cap": 1.05, # Prevent environment from rescuing the initial state
|
| 162 |
+
"soft_pr_cap": 1.10,
|
| 163 |
},
|
| 164 |
"task4": {
|
| 165 |
"aL": 0.60,
|
| 166 |
+
"sL": 0.00,
|
| 167 |
+
"aD": 0.18,
|
| 168 |
+
"sD": 0.08,
|
| 169 |
+
"aT": 0.22,
|
| 170 |
+
"sT": 0.10,
|
| 171 |
+
"aM": 0.68,
|
| 172 |
+
"sM": 0.08,
|
| 173 |
+
"gap_scale": 0.14,
|
| 174 |
+
"d_max": 0.32,
|
| 175 |
+
"s_base": 0.05,
|
| 176 |
+
"s_gain": 0.14,
|
| 177 |
+
"f_bias": 0.32,
|
| 178 |
+
"t_task_bias": 0.02,
|
| 179 |
+
"soft_t_cap": 0.85,
|
| 180 |
+
"soft_pr_cap": 0.89,
|
| 181 |
},
|
| 182 |
}
|
| 183 |
|