Skip to main content

quantrs2_ml/
reinforcement.rs

1//! Quantum Reinforcement Learning (QRL) agents and environments.
2//!
3//! Defines the [`Environment`] trait plus [`QuantumRLAgent`] which uses a
4//! quantum neural network as its policy / value function approximator,
5//! trained via policy gradient or Q-learning objectives.
6
7use crate::error::{MLError, Result};
8use crate::qnn::QuantumNeuralNetwork;
9use scirs2_core::ndarray::{Array1, Array2};
10use scirs2_core::random::prelude::*;
11use std::collections::HashMap;
12
13/// Environment for reinforcement learning
14pub trait Environment {
15    /// Gets the current state
16    fn state(&self) -> Array1<f64>;
17
18    /// Gets the number of available actions
19    fn num_actions(&self) -> usize;
20
21    /// Takes an action and returns the reward and next state
22    fn step(&mut self, action: usize) -> Result<(Array1<f64>, f64, bool)>;
23
24    /// Resets the environment
25    fn reset(&mut self) -> Array1<f64>;
26}
27
28/// Agent for reinforcement learning
29pub trait QuantumAgent {
30    /// Gets an action for a given state
31    fn get_action(&self, state: &Array1<f64>) -> Result<usize>;
32
33    /// Updates the agent based on a reward
34    fn update(
35        &mut self,
36        state: &Array1<f64>,
37        action: usize,
38        reward: f64,
39        next_state: &Array1<f64>,
40        done: bool,
41    ) -> Result<()>;
42
43    /// Trains the agent on an environment
44    fn train(&mut self, env: &mut dyn Environment, episodes: usize) -> Result<f64>;
45
46    /// Evaluates the agent on an environment
47    fn evaluate(&self, env: &mut dyn Environment, episodes: usize) -> Result<f64>;
48}
49
50/// Reinforcement learning algorithm type
51#[derive(Debug, Clone, Copy)]
52pub enum ReinforcementLearningType {
53    /// Q-learning
54    QLearning,
55
56    /// SARSA
57    SARSA,
58
59    /// Deep Q-Network
60    DQN,
61
62    /// Policy Gradient
63    PolicyGradient,
64
65    /// Quantum Approximate Optimization Algorithm
66    QAOA,
67}
68
69/// Reinforcement learning with quantum circuit
70#[derive(Debug, Clone)]
71pub struct ReinforcementLearning {
72    /// Type of reinforcement learning algorithm
73    rl_type: ReinforcementLearningType,
74
75    /// Quantum neural network
76    qnn: QuantumNeuralNetwork,
77
78    /// Learning rate
79    learning_rate: f64,
80
81    /// Discount factor
82    discount_factor: f64,
83
84    /// Exploration rate
85    exploration_rate: f64,
86
87    /// Number of state dimensions
88    state_dim: usize,
89
90    /// Number of actions
91    action_dim: usize,
92}
93
94impl ReinforcementLearning {
95    /// Creates a new quantum reinforcement learning agent
96    ///
97    /// # Errors
98    /// Returns an error if the quantum neural network cannot be created
99    pub fn new() -> Result<Self> {
100        // This is a placeholder implementation
101        // In a real system, this would create a proper QNN
102
103        let layers = vec![
104            crate::qnn::QNNLayerType::EncodingLayer { num_features: 4 },
105            crate::qnn::QNNLayerType::VariationalLayer { num_params: 16 },
106            crate::qnn::QNNLayerType::EntanglementLayer {
107                connectivity: "full".to_string(),
108            },
109            crate::qnn::QNNLayerType::VariationalLayer { num_params: 16 },
110            crate::qnn::QNNLayerType::MeasurementLayer {
111                measurement_basis: "computational".to_string(),
112            },
113        ];
114
115        let qnn = QuantumNeuralNetwork::new(
116            layers, 8, // 8 qubits
117            4, // 4 input features
118            2, // 2 output actions
119        )?;
120
121        Ok(ReinforcementLearning {
122            rl_type: ReinforcementLearningType::QLearning,
123            qnn,
124            learning_rate: 0.01,
125            discount_factor: 0.95,
126            exploration_rate: 0.1,
127            state_dim: 4,
128            action_dim: 2,
129        })
130    }
131
132    /// Sets the reinforcement learning algorithm type
133    pub fn with_algorithm(mut self, rl_type: ReinforcementLearningType) -> Self {
134        self.rl_type = rl_type;
135        self
136    }
137
138    /// Sets the state dimension
139    pub fn with_state_dimension(mut self, state_dim: usize) -> Self {
140        self.state_dim = state_dim;
141        self
142    }
143
144    /// Sets the action dimension
145    pub fn with_action_dimension(mut self, action_dim: usize) -> Self {
146        self.action_dim = action_dim;
147        self
148    }
149
150    /// Sets the learning rate
151    pub fn with_learning_rate(mut self, learning_rate: f64) -> Self {
152        self.learning_rate = learning_rate;
153        self
154    }
155
156    /// Sets the discount factor
157    pub fn with_discount_factor(mut self, discount_factor: f64) -> Self {
158        self.discount_factor = discount_factor;
159        self
160    }
161
162    /// Sets the exploration rate
163    pub fn with_exploration_rate(mut self, exploration_rate: f64) -> Self {
164        self.exploration_rate = exploration_rate;
165        self
166    }
167
168    /// Gets the Q-values for a state by evaluating the quantum neural network.
169    ///
170    /// The state is encoded into the QNN's parameterised circuit, simulated on
171    /// the state-vector backend, and each action's Q-value is read out as a
172    /// Pauli expectation value (one output per action).
173    fn get_q_values(&self, state: &Array1<f64>) -> Result<Array1<f64>> {
174        self.qnn.forward(state)
175    }
176}
177
178impl QuantumAgent for ReinforcementLearning {
179    fn get_action(&self, state: &Array1<f64>) -> Result<usize> {
180        // Epsilon-greedy action selection
181        if thread_rng().random::<f64>() < self.exploration_rate {
182            // Explore: random action
183            Ok(fastrand::usize(0..self.action_dim))
184        } else {
185            // Exploit: best action according to the QNN Q-values.
186            let q_values = self.get_q_values(state)?;
187            if q_values.is_empty() {
188                return Err(MLError::MLOperationError(
189                    "QNN produced no Q-values".to_string(),
190                ));
191            }
192            let mut best_action = 0;
193            for i in 1..q_values.len() {
194                if q_values[i] > q_values[best_action] {
195                    best_action = i;
196                }
197            }
198
199            Ok(best_action)
200        }
201    }
202
203    fn update(
204        &mut self,
205        state: &Array1<f64>,
206        action: usize,
207        reward: f64,
208        next_state: &Array1<f64>,
209        done: bool,
210    ) -> Result<()> {
211        // Semi-gradient Q-learning update on the quantum neural network.
212        let q_values = self.get_q_values(state)?;
213        if action >= q_values.len() {
214            return Err(MLError::InvalidParameter(format!(
215                "action {action} out of range for {} Q-values",
216                q_values.len()
217            )));
218        }
219        let q_sa = q_values[action];
220
221        // Bellman target: r + γ · max_a' Q(s', a')  (terminal transitions use r).
222        let target = if done {
223            reward
224        } else {
225            let next_q = self.get_q_values(next_state)?;
226            let max_next = next_q.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
227            reward + self.discount_factor * max_next
228        };
229
230        // Loss L = (Q(s,a) - target)²  ⇒  dL/dθ = 2·(Q(s,a) - target)·∂Q(s,a)/∂θ,
231        // where ∂Q(s,a)/∂θ is obtained by the parameter-shift rule.
232        let td_error = q_sa - target;
233        let gradient = self.qnn.output_component_gradient(state, action)?;
234        for j in 0..self.qnn.parameters.len() {
235            self.qnn.parameters[j] -= self.learning_rate * 2.0 * td_error * gradient[j];
236        }
237
238        Ok(())
239    }
240
241    fn train(&mut self, env: &mut dyn Environment, episodes: usize) -> Result<f64> {
242        let mut total_reward = 0.0;
243
244        for _ in 0..episodes {
245            let mut state = env.reset();
246            let mut episode_reward = 0.0;
247            let mut done = false;
248
249            while !done {
250                let action = self.get_action(&state)?;
251                let (next_state, reward, is_done) = env.step(action)?;
252
253                self.update(&state, action, reward, &next_state, is_done)?;
254
255                state = next_state;
256                episode_reward += reward;
257                done = is_done;
258            }
259
260            total_reward += episode_reward;
261        }
262
263        Ok(total_reward / episodes as f64)
264    }
265
266    fn evaluate(&self, env: &mut dyn Environment, episodes: usize) -> Result<f64> {
267        let mut total_reward = 0.0;
268
269        for _ in 0..episodes {
270            let mut state = env.reset();
271            let mut episode_reward = 0.0;
272            let mut done = false;
273
274            while !done {
275                let action = self.get_action(&state)?;
276                let (next_state, reward, is_done) = env.step(action)?;
277
278                state = next_state;
279                episode_reward += reward;
280                done = is_done;
281            }
282
283            total_reward += episode_reward;
284        }
285
286        Ok(total_reward / episodes as f64)
287    }
288}
289
290/// GridWorld environment for testing reinforcement learning
291pub struct GridWorldEnvironment {
292    /// Width of the grid
293    width: usize,
294
295    /// Height of the grid
296    height: usize,
297
298    /// Current position (x, y)
299    position: (usize, usize),
300
301    /// Goal position (x, y)
302    goal: (usize, usize),
303
304    /// Obstacle positions (x, y)
305    obstacles: Vec<(usize, usize)>,
306}
307
308impl GridWorldEnvironment {
309    /// Creates a new GridWorld environment
310    pub fn new(width: usize, height: usize) -> Self {
311        GridWorldEnvironment {
312            width,
313            height,
314            position: (0, 0),
315            goal: (width - 1, height - 1),
316            obstacles: Vec::new(),
317        }
318    }
319
320    /// Sets the goal position
321    pub fn with_goal(mut self, x: usize, y: usize) -> Self {
322        self.goal = (x.min(self.width - 1), y.min(self.height - 1));
323        self
324    }
325
326    /// Sets the obstacles
327    pub fn with_obstacles(mut self, obstacles: Vec<(usize, usize)>) -> Self {
328        self.obstacles = obstacles;
329        self
330    }
331
332    /// Checks if a position is an obstacle
333    pub fn is_obstacle(&self, x: usize, y: usize) -> bool {
334        self.obstacles.contains(&(x, y))
335    }
336
337    /// Checks if a position is the goal
338    pub fn is_goal(&self, x: usize, y: usize) -> bool {
339        (x, y) == self.goal
340    }
341}
342
343impl Environment for GridWorldEnvironment {
344    fn state(&self) -> Array1<f64> {
345        let mut state = Array1::zeros(4);
346
347        // Normalize position
348        state[0] = self.position.0 as f64 / self.width as f64;
349        state[1] = self.position.1 as f64 / self.height as f64;
350
351        // Normalize goal
352        state[2] = self.goal.0 as f64 / self.width as f64;
353        state[3] = self.goal.1 as f64 / self.height as f64;
354
355        state
356    }
357
358    fn num_actions(&self) -> usize {
359        4 // Up, Right, Down, Left
360    }
361
362    fn step(&mut self, action: usize) -> Result<(Array1<f64>, f64, bool)> {
363        // Calculate new position
364        let (x, y) = self.position;
365        let (new_x, new_y) = match action {
366            0 => (x, y.saturating_sub(1)), // Up
367            1 => (x + 1, y),               // Right
368            2 => (x, y + 1),               // Down
369            3 => (x.saturating_sub(1), y), // Left
370            _ => {
371                return Err(MLError::InvalidParameter(format!(
372                    "Invalid action: {}",
373                    action
374                )))
375            }
376        };
377
378        // Check if new position is valid
379        let new_x = new_x.min(self.width - 1);
380        let new_y = new_y.min(self.height - 1);
381
382        // Check if new position is an obstacle
383        if self.obstacles.contains(&(new_x, new_y)) {
384            // Stay in the same position
385            let reward = -1.0;
386            let done = false;
387            return Ok((self.state(), reward, done));
388        }
389
390        // Update position
391        self.position = (new_x, new_y);
392
393        // Calculate reward
394        let reward = if (new_x, new_y) == self.goal {
395            10.0 // Goal reached
396        } else {
397            -0.1 // Step penalty
398        };
399
400        // Check if done
401        let done = (new_x, new_y) == self.goal;
402
403        Ok((self.state(), reward, done))
404    }
405
406    fn reset(&mut self) -> Array1<f64> {
407        self.position = (0, 0);
408        self.state()
409    }
410}