File size: 13,346 Bytes
cb922bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
"""
Cloud Resource Allocation RL Environment.

This module implements the core environment class for simulating cloud infrastructure
resource allocation. The environment maintains system state (CPU, memory, request rate,
allocated resources) and processes actions to dynamically adjust resource allocation.
"""

import numpy as np
import torch
from typing import Tuple, Dict, List, Optional
from env.config import EnvConfig
from env.reward import RewardCalculator


class CloudResourceEnv:
    """
    Simulates a cloud infrastructure environment for RL-based resource allocation.
    
    The environment models a cloud system where an agent must dynamically allocate
    server instances based on observed system metrics. The state includes CPU utilization,
    memory utilization, incoming request rate, and currently allocated resources.
    
    State space: [cpu_util, memory_util, request_rate, allocated_resources]
    Action space: {0: decrease, 1: maintain, 2: increase}
    
    The environment incorporates stochastic dynamics to simulate real-world variability
    in request patterns and system behavior.
    """
    
    # Action constants
    ACTION_DECREASE = 0
    ACTION_MAINTAIN = 1
    ACTION_INCREASE = 2
    
    def __init__(
        self,
        config: Optional[EnvConfig] = None,
        reward_calculator: Optional[RewardCalculator] = None,
        task_name: str = "medium"
    ):
        """
        Initialize the cloud resource allocation environment.
        
        Args:
            config: Environment configuration parameters. If None, uses default EnvConfig.
            reward_calculator: Reward calculator instance. If None, creates default RewardCalculator.
            task_name: Task difficulty level ("easy", "medium", or "hard"). Defaults to "medium".
        
        Raises:
            ValueError: If task_name is not in TASKS dictionary.
        """
        # Import TASKS here to avoid circular imports
        from env.config import TASKS
        
        # Validate task_name
        if task_name not in TASKS:
            raise ValueError(
                f"Invalid task_name '{task_name}'. Must be one of: {list(TASKS.keys())}"
            )
        
        # Use default config if not provided
        self.config = config if config is not None else EnvConfig()
        
        # Override config with task-specific parameters
        task_config = TASKS[task_name]
        self.config.base_request_rate = task_config["base_request_rate"]
        self.config.request_rate_std = task_config["request_rate_std"]
        self.task_name = task_name
        
        # Initialize reward calculator
        self.reward_calculator = reward_calculator if reward_calculator is not None else RewardCalculator()
        
        # Initialize state variables
        self.cpu_util: float = 0.0
        self.memory_util: float = 0.0
        self.request_rate: int = 0
        self.allocated_resources: int = self.config.initial_resources
        self.latency: float = 0.0  # Latency in ms
        
        # Initialize episode tracking
        self.current_step: int = 0
        self.episode_states: List[np.ndarray] = []
        self.episode_actions: List[int] = []
        self.episode_rewards: List[float] = []
        self.cumulative_reward: float = 0.0
        
        # Episode termination flag
        self.done: bool = False
        
        # Random number generator for reproducibility
        self.rng = np.random.RandomState()
    
    def reset(self) -> np.ndarray:
        """
        Reset environment to initial state.
        
        This method reinitializes the environment for a new episode. It resets all
        state variables to their initial values, clears episode history, and introduces
        stochastic variation in the initial request rate to ensure diverse starting
        conditions across episodes.
        
        Returns:
            Initial observation as numpy array [cpu_util, memory_util, request_rate, allocated_resources]
        """
        # Reset episode tracking
        self.current_step = 0
        self.episode_states = []
        self.episode_actions = []
        self.episode_rewards = []
        self.cumulative_reward = 0.0
        self.done = False
        
        # Reset allocated resources to initial value
        self.allocated_resources = self.config.initial_resources
        
        # Initialize request rate with stochastic variation
        # This ensures different starting conditions across episodes
        self.request_rate = int(
            self.config.base_request_rate + 
            self.rng.normal(0, self.config.request_rate_std)
        )
        # Ensure request rate is non-negative
        self.request_rate = max(0, self.request_rate)
        
        # Calculate initial CPU and memory utilization
        # Utilization depends on request rate and allocated resources
        self.cpu_util = (self.request_rate * self.config.cpu_per_request) / (
            self.allocated_resources * self.config.resource_capacity
        ) * 100.0
        
        self.memory_util = (self.request_rate * self.config.memory_per_request) / (
            self.allocated_resources * self.config.resource_capacity
        ) * 100.0
        
        # Clamp utilization values to valid range [0, 100]
        self.cpu_util = max(0.0, min(100.0, self.cpu_util))
        self.memory_util = max(0.0, min(100.0, self.memory_util))
        
        # Calculate latency: higher request rate and fewer resources increase latency
        self.latency = self.request_rate / max(self.allocated_resources, 1)
        
        # Return initial observation
        return self._get_observation()
    
    def _get_observation(self) -> np.ndarray:
        """
        Get current state as observation array.
        
        Returns:
            Numpy array [cpu_util, memory_util, request_rate, allocated_resources]
        """
        return np.array([
            self.cpu_util,
            self.memory_util,
            float(self.request_rate),
            float(self.allocated_resources)
        ], dtype=np.float32)
    
    def get_state_tensor(self) -> torch.Tensor:
        """
        Get current state as PyTorch tensor.
        
        This method provides a PyTorch tensor representation of the current state,
        enabling seamless integration with PyTorch-based RL algorithms and neural networks.
        The tensor maintains the same structure as the NumPy observation array.
        
        Returns:
            PyTorch tensor [cpu_util, memory_util, request_rate, allocated_resources]
        """
        observation = self._get_observation()
        return torch.from_numpy(observation)
    
    def state(self):
        """
        Get current state as EnvState instance for OpenEnv compliance.
        
        This method provides typed, validated access to the environment state
        using the Pydantic EnvState model. It can be called after reset() or step()
        to retrieve the current state in a structured format.
        
        Returns:
            EnvState: Pydantic model with cpu, memory, request_rate, and resources fields
        """
        from env.config import EnvState
        
        return EnvState(
            cpu=self.cpu_util,
            memory=self.memory_util,
            request_rate=float(self.request_rate),
            resources=self.allocated_resources
        )
    
    def _update_state(self, action: int) -> None:
        """
        Update system state based on action and stochastic dynamics.
        
        This method applies the selected action to modify allocated resources,
        updates the request rate with stochastic fluctuation, and recalculates
        CPU and memory utilization based on the new system configuration.
        
        Args:
            action: Action to execute (0: decrease, 1: maintain, 2: increase)
        """
        # Apply action to allocated resources
        if action == self.ACTION_INCREASE:
            self.allocated_resources += 1
        elif action == self.ACTION_DECREASE:
            # Maintain minimum of 1 resource
            self.allocated_resources = max(1, self.allocated_resources - 1)
        elif action == self.ACTION_MAINTAIN:
            # Keep resources unchanged
            pass
        else:
            raise ValueError(f"Invalid action: {action}. Must be 0, 1, or 2.")
        
        # Update request rate with stochastic fluctuation
        # This simulates real-world variability in incoming traffic
        fluctuation = self.rng.normal(0, self.config.request_rate_std)
        self.request_rate = int(self.config.base_request_rate + fluctuation)
        # Ensure request rate is non-negative
        self.request_rate = max(0, self.request_rate)
        
        # Calculate CPU utilization based on request rate and allocated resources
        # Higher request rate increases utilization
        # More allocated resources decrease utilization
        self.cpu_util = (self.request_rate * self.config.cpu_per_request) / (
            self.allocated_resources * self.config.resource_capacity
        ) * 100.0
        
        # Calculate memory utilization based on request rate and allocated resources
        self.memory_util = (self.request_rate * self.config.memory_per_request) / (
            self.allocated_resources * self.config.resource_capacity
        ) * 100.0
        
        # Clamp all values to valid ranges
        self.cpu_util = max(0.0, min(100.0, self.cpu_util))
        self.memory_util = max(0.0, min(100.0, self.memory_util))
        
        # Calculate latency: higher request rate and fewer resources increase latency
        self.latency = self.request_rate / max(self.allocated_resources, 1)
    
    def _check_termination(self) -> bool:
        """
        Check if episode should terminate based on state or step count.
        
        Termination conditions:
        1. Step count exceeds max_steps
        2. CPU utilization exceeds termination threshold (default 95%)
        3. Memory utilization exceeds termination threshold (default 95%)
        
        Returns:
            True if episode should terminate, False otherwise
        """
        # Check if max steps exceeded
        if self.current_step >= self.config.max_steps:
            return True
        
        # Check if CPU utilization exceeds threshold
        if self.cpu_util > self.config.termination_threshold:
            return True
        
        # Check if memory utilization exceeds threshold
        if self.memory_util > self.config.termination_threshold:
            return True
        
        return False
    
    def step(self, action: int) -> Tuple[np.ndarray, float, bool, Dict]:
        """
        Execute one time step with the given action.
        
        This method processes the action, updates the environment state, calculates
        the reward, checks for termination, and stores the transition in episode
        history. It returns the standard RL tuple (observation, reward, done, info).
        
        Args:
            action: Action to execute (0: decrease, 1: maintain, 2: increase)
        
        Returns:
            observation: Current state after action as numpy array
            reward: Reward signal from reward calculator
            done: Whether episode has terminated
            info: Additional diagnostic information dictionary
        
        Raises:
            ValueError: If action is not in valid range {0, 1, 2}
            RuntimeError: If step is called after episode termination
        """
        # Check if episode already terminated
        if self.done:
            raise RuntimeError(
                "Cannot call step() after episode termination. Call reset() to start a new episode."
            )
        
        # Validate action is in valid range
        if action not in {self.ACTION_DECREASE, self.ACTION_MAINTAIN, self.ACTION_INCREASE}:
            raise ValueError(
                f"Invalid action: {action}. Must be 0 (decrease), 1 (maintain), or 2 (increase)."
            )
        
        # Update state based on action
        self._update_state(action)
        
        # Increment step counter
        self.current_step += 1
        
        # Calculate reward
        reward = self.reward_calculator.calculate_reward(
            self.cpu_util,
            self.memory_util,
            self.allocated_resources
        )
        
        # Check termination status
        self.done = self._check_termination()
        
        # Get current observation
        observation = self._get_observation()
        
        # Store transition in episode history
        self.episode_states.append(observation.copy())
        self.episode_actions.append(action)
        self.episode_rewards.append(reward)
        self.cumulative_reward += reward
        
        # Prepare info dictionary
        info = {
            'step': self.current_step,
            'cumulative_reward': self.cumulative_reward,
            'cpu_util': self.cpu_util,
            'memory_util': self.memory_util,
            'request_rate': self.request_rate,
            'allocated_resources': self.allocated_resources,
            'latency': self.latency
        }
        
        return observation, reward, self.done, info