Skip to main content

quantrs2_tytan/sampler/hardware/
azure_quantum.rs

1//! Azure Quantum Sampler Implementation
2//!
3//! This module provides integration with Microsoft Azure Quantum
4//! for solving optimization problems using various quantum and quantum-inspired solvers.
5
6use scirs2_core::ndarray::{Array, Ix2};
7use scirs2_core::random::{thread_rng, Rng, RngExt};
8use std::collections::HashMap;
9
10use quantrs2_anneal::QuboModel;
11
12use super::super::{SampleResult, Sampler, SamplerError, SamplerResult};
13
14/// Azure Quantum solver types
15#[derive(Debug, Clone)]
16pub enum AzureSolver {
17    /// Microsoft QIO - Simulated Annealing
18    SimulatedAnnealing,
19    /// Microsoft QIO - Parallel Tempering
20    ParallelTempering,
21    /// Microsoft QIO - Tabu Search
22    TabuSearch,
23    /// Microsoft QIO - Population Annealing
24    PopulationAnnealing,
25    /// Microsoft QIO - Substrate Monte Carlo
26    SubstrateMonteCarlo,
27    /// IonQ quantum computer
28    IonQ,
29    /// Quantinuum (Honeywell) quantum computer
30    Quantinuum,
31    /// Rigetti quantum computer
32    Rigetti,
33}
34
35/// Azure Quantum Sampler Configuration
36#[derive(Debug, Clone)]
37pub struct AzureQuantumConfig {
38    /// Azure subscription ID
39    pub subscription_id: String,
40    /// Resource group name
41    pub resource_group: String,
42    /// Workspace name
43    pub workspace_name: String,
44    /// Solver to use
45    pub solver: AzureSolver,
46    /// Timeout in seconds
47    pub timeout: u64,
48    /// Additional solver-specific parameters
49    pub solver_params: HashMap<String, String>,
50}
51
52impl Default for AzureQuantumConfig {
53    fn default() -> Self {
54        Self {
55            subscription_id: String::new(),
56            resource_group: String::new(),
57            workspace_name: String::new(),
58            solver: AzureSolver::SimulatedAnnealing,
59            timeout: 300,
60            solver_params: HashMap::new(),
61        }
62    }
63}
64
65/// Azure Quantum Sampler
66///
67/// This sampler connects to Microsoft Azure Quantum to solve QUBO problems
68/// using various quantum and quantum-inspired optimization solvers.
69pub struct AzureQuantumSampler {
70    config: AzureQuantumConfig,
71}
72
73impl AzureQuantumSampler {
74    /// Create a new Azure Quantum sampler
75    ///
76    /// # Arguments
77    ///
78    /// * `config` - The Azure Quantum configuration
79    #[must_use]
80    pub const fn new(config: AzureQuantumConfig) -> Self {
81        Self { config }
82    }
83
84    /// Create a new Azure Quantum sampler with workspace details
85    ///
86    /// # Arguments
87    ///
88    /// * `subscription_id` - Azure subscription ID
89    /// * `resource_group` - Resource group name
90    /// * `workspace_name` - Workspace name
91    #[must_use]
92    pub fn with_workspace(
93        subscription_id: &str,
94        resource_group: &str,
95        workspace_name: &str,
96    ) -> Self {
97        Self {
98            config: AzureQuantumConfig {
99                subscription_id: subscription_id.to_string(),
100                resource_group: resource_group.to_string(),
101                workspace_name: workspace_name.to_string(),
102                ..Default::default()
103            },
104        }
105    }
106
107    /// Set the solver to use
108    #[must_use]
109    pub const fn with_solver(mut self, solver: AzureSolver) -> Self {
110        self.config.solver = solver;
111        self
112    }
113
114    /// Set the timeout
115    #[must_use]
116    pub const fn with_timeout(mut self, timeout: u64) -> Self {
117        self.config.timeout = timeout;
118        self
119    }
120
121    /// Add a solver-specific parameter
122    #[must_use]
123    pub fn with_param(mut self, key: String, value: String) -> Self {
124        self.config.solver_params.insert(key, value);
125        self
126    }
127}
128
129impl Sampler for AzureQuantumSampler {
130    fn run_qubo(
131        &self,
132        qubo: &(Array<f64, Ix2>, HashMap<String, usize>),
133        shots: usize,
134    ) -> SamplerResult<Vec<SampleResult>> {
135        // Extract matrix and variable mapping
136        let (matrix, var_map) = qubo;
137
138        // Get the problem dimension
139        let n_vars = var_map.len();
140
141        // Validate problem size based on solver
142        match self.config.solver {
143            AzureSolver::IonQ => {
144                if n_vars > 29 {
145                    return Err(SamplerError::InvalidParameter(
146                        "IonQ currently supports up to 29 qubits".to_string(),
147                    ));
148                }
149            }
150            AzureSolver::Quantinuum => {
151                if n_vars > 20 {
152                    return Err(SamplerError::InvalidParameter(
153                        "Quantinuum currently supports up to 20 qubits for this application"
154                            .to_string(),
155                    ));
156                }
157            }
158            AzureSolver::Rigetti => {
159                if n_vars > 40 {
160                    return Err(SamplerError::InvalidParameter(
161                        "Rigetti currently supports up to 40 qubits".to_string(),
162                    ));
163                }
164            }
165            _ => {
166                // QIO solvers can handle larger problems
167                if n_vars > 10000 {
168                    return Err(SamplerError::InvalidParameter(
169                        "Problem size exceeds Azure QIO limits".to_string(),
170                    ));
171                }
172            }
173        }
174
175        // Map from indices back to variable names
176        let idx_to_var: HashMap<usize, String> = var_map
177            .iter()
178            .map(|(var, &idx)| (idx, var.clone()))
179            .collect();
180
181        // Convert ndarray to a QuboModel
182        let mut qubo_model = QuboModel::new(n_vars);
183
184        // Set linear and quadratic terms
185        for i in 0..n_vars {
186            if matrix[[i, i]] != 0.0 {
187                qubo_model.set_linear(i, matrix[[i, i]])?;
188            }
189
190            for j in (i + 1)..n_vars {
191                if matrix[[i, j]] != 0.0 {
192                    qubo_model.set_quadratic(i, j, matrix[[i, j]])?;
193                }
194            }
195        }
196
197        // Azure Quantum REST API integration
198        #[cfg(feature = "azure_quantum")]
199        {
200            // Validate workspace credentials before any HTTP calls
201            if self.config.subscription_id.is_empty()
202                || self.config.resource_group.is_empty()
203                || self.config.workspace_name.is_empty()
204            {
205                return Err(SamplerError::ApiError(
206                    "Azure Quantum workspace not configured. Call with_workspace() to provide \
207                     subscription_id, resource_group, and workspace_name."
208                        .to_string(),
209                ));
210            }
211
212            // Determine the provider and target for the selected solver
213            let (provider_id, target_id) = match self.config.solver {
214                AzureSolver::SimulatedAnnealing => {
215                    ("microsoft.qio", "microsoft.simulatedannealing.cpu")
216                }
217                AzureSolver::ParallelTempering => {
218                    ("microsoft.qio", "microsoft.paralleltempering.cpu")
219                }
220                AzureSolver::TabuSearch => ("microsoft.qio", "microsoft.tabu.cpu"),
221                AzureSolver::PopulationAnnealing => {
222                    ("microsoft.qio", "microsoft.populationannealing.cpu")
223                }
224                AzureSolver::SubstrateMonteCarlo => {
225                    ("microsoft.qio", "microsoft.substochastic.cpu")
226                }
227                AzureSolver::IonQ => ("ionq", "ionq.qpu"),
228                AzureSolver::Quantinuum => ("quantinuum", "quantinuum.hqs-lt-s1"),
229                AzureSolver::Rigetti => ("rigetti", "rigetti.qpu.aspen-m-3"),
230            };
231
232            // Build the QIO-format cost function payload
233            let terms: Vec<serde_json::Value> = {
234                let mut t = Vec::new();
235                for i in 0..n_vars {
236                    if matrix[[i, i]] != 0.0 {
237                        t.push(serde_json::json!({
238                            "c": matrix[[i, i]],
239                            "ids": [i]
240                        }));
241                    }
242                    for j in (i + 1)..n_vars {
243                        if matrix[[i, j]] != 0.0 {
244                            t.push(serde_json::json!({
245                                "c": matrix[[i, j]],
246                                "ids": [i, j]
247                            }));
248                        }
249                    }
250                }
251                t
252            };
253
254            let problem_payload = serde_json::json!({
255                "cost_function": {
256                    "type": "ising",
257                    "version": "1.1",
258                    "terms": terms
259                }
260            });
261
262            let mut solver_params = serde_json::json!({
263                "timeout": self.config.timeout,
264                "seed": 42u64
265            });
266
267            // Merge user-supplied solver params
268            for (k, v) in &self.config.solver_params {
269                if let Some(obj) = solver_params.as_object_mut() {
270                    obj.insert(k.clone(), serde_json::Value::String(v.clone()));
271                }
272            }
273
274            let job_payload = serde_json::json!({
275                "id": uuid::Uuid::new_v4().to_string(),
276                "name": "qubo_job",
277                "providerId": provider_id,
278                "target": target_id,
279                "inputDataFormat": "microsoft.qio.v2",
280                "outputDataFormat": "microsoft.qio-results.v2",
281                "inputParams": solver_params,
282                "inputData": problem_payload
283            });
284
285            // Azure Quantum REST API base URL
286            let base_url = format!(
287                "https://{region}.quantum.azure.com/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Quantum/Workspaces/{ws}",
288                region = "eastus",
289                sub = self.config.subscription_id,
290                rg = self.config.resource_group,
291                ws = self.config.workspace_name
292            );
293            let jobs_url = format!("{base_url}/jobs");
294
295            let client = reqwest::blocking::Client::builder()
296                .timeout(std::time::Duration::from_secs(30))
297                .build()
298                .map_err(|e| SamplerError::ApiError(format!("Failed to build HTTP client: {e}")))?;
299
300            // Submit the job
301            let submit_resp = client
302                .post(&jobs_url)
303                .header("Content-Type", "application/json")
304                .json(&job_payload)
305                .send()
306                .map_err(|e| {
307                    SamplerError::ApiError(format!(
308                        "Failed to submit Azure Quantum job: {e}. \
309                     Ensure workspace credentials are correct and network is accessible."
310                    ))
311                })?;
312
313            if !submit_resp.status().is_success() {
314                let status = submit_resp.status();
315                let body = submit_resp
316                    .text()
317                    .unwrap_or_else(|_| "<unreadable>".to_string());
318                return Err(SamplerError::ApiError(format!(
319                    "Azure Quantum job submission failed (HTTP {status}): {body}"
320                )));
321            }
322
323            let job_response: serde_json::Value = submit_resp.json().map_err(|e| {
324                SamplerError::ApiError(format!("Failed to parse Azure Quantum response: {e}"))
325            })?;
326
327            let job_id = job_response["id"]
328                .as_str()
329                .ok_or_else(|| {
330                    SamplerError::ApiError("Missing job ID in Azure Quantum response".to_string())
331                })?
332                .to_string();
333
334            // Poll for job completion
335            let poll_interval = 5u64;
336            let max_polls = self.config.timeout / poll_interval + 1;
337            let mut poll_count = 0u64;
338            loop {
339                if poll_count >= max_polls {
340                    return Err(SamplerError::ApiError(format!(
341                        "Azure Quantum job {job_id} timed out after {max_polls} polls ({}s)",
342                        self.config.timeout
343                    )));
344                }
345                poll_count += 1;
346                std::thread::sleep(std::time::Duration::from_secs(poll_interval));
347
348                let status_url = format!("{jobs_url}/{job_id}");
349                let status_resp = client.get(&status_url).send().map_err(|e| {
350                    SamplerError::ApiError(format!("Failed to poll Azure job status: {e}"))
351                })?;
352
353                let status_json: serde_json::Value = status_resp.json().map_err(|e| {
354                    SamplerError::ApiError(format!("Failed to parse Azure status: {e}"))
355                })?;
356
357                match status_json["status"].as_str() {
358                    Some("Succeeded") => break,
359                    Some("Failed") => {
360                        let reason = status_json["errorData"]["message"]
361                            .as_str()
362                            .unwrap_or("unknown error");
363                        return Err(SamplerError::ApiError(format!(
364                            "Azure Quantum job failed: {reason}"
365                        )));
366                    }
367                    Some("Cancelled") => {
368                        return Err(SamplerError::ApiError(
369                            "Azure Quantum job was cancelled".to_string(),
370                        ));
371                    }
372                    _ => continue,
373                }
374            }
375
376            // Retrieve results
377            let output_url = format!("{jobs_url}/{job_id}/output");
378            let output_resp = client.get(&output_url).send().map_err(|e| {
379                SamplerError::ApiError(format!("Failed to retrieve Azure results: {e}"))
380            })?;
381
382            let output_json: serde_json::Value = output_resp.json().map_err(|e| {
383                SamplerError::ApiError(format!("Failed to parse Azure result: {e}"))
384            })?;
385
386            // Parse QIO result format: solutions array with configuration and cost
387            if let Some(solutions_arr) = output_json["solutions"].as_array() {
388                let mut parsed: Vec<SampleResult> = solutions_arr
389                    .iter()
390                    .map(|sol| {
391                        let energy = sol["cost"].as_f64().unwrap_or(0.0);
392                        let occurrences = sol["count"].as_u64().unwrap_or(1) as usize;
393                        let assignments: HashMap<String, bool> =
394                            if let Some(config_obj) = sol["configuration"].as_object() {
395                                config_obj
396                                    .iter()
397                                    .filter_map(|(k, v)| {
398                                        k.parse::<usize>().ok().and_then(|idx| {
399                                            idx_to_var.get(&idx).map(|name| {
400                                                (name.clone(), v.as_i64().unwrap_or(0) > 0)
401                                            })
402                                        })
403                                    })
404                                    .collect()
405                            } else {
406                                HashMap::new()
407                            };
408                        SampleResult {
409                            assignments,
410                            energy,
411                            occurrences,
412                        }
413                    })
414                    .collect();
415
416                parsed.sort_by(|a, b| {
417                    a.energy
418                        .partial_cmp(&b.energy)
419                        .unwrap_or(std::cmp::Ordering::Equal)
420                });
421
422                return Ok(parsed);
423            }
424            // Fall through to simulation path if result parsing fails
425        }
426
427        // Placeholder implementation - simulate Azure Quantum behavior
428        let mut results = Vec::new();
429        let mut rng = thread_rng();
430
431        // Different solvers have different characteristics
432        let unique_solutions = match self.config.solver {
433            AzureSolver::SimulatedAnnealing => shots.min(50),
434            AzureSolver::ParallelTempering => shots.min(100),
435            AzureSolver::TabuSearch => shots.min(30),
436            AzureSolver::PopulationAnnealing => shots.min(200),
437            AzureSolver::SubstrateMonteCarlo => shots.min(150),
438            AzureSolver::IonQ | AzureSolver::Quantinuum | AzureSolver::Rigetti => {
439                // Quantum hardware typically provides many measurement samples
440                shots.min(1000)
441            }
442        };
443
444        for _ in 0..unique_solutions {
445            let assignments: HashMap<String, bool> = idx_to_var
446                .values()
447                .map(|name| (name.clone(), rng.random::<bool>()))
448                .collect();
449
450            // Calculate energy
451            let mut energy = 0.0;
452            for (var_name, &val) in &assignments {
453                let i = var_map[var_name];
454                if val {
455                    energy += matrix[[i, i]];
456                    for (other_var, &other_val) in &assignments {
457                        let j = var_map[other_var];
458                        if i < j && other_val {
459                            energy += matrix[[i, j]];
460                        }
461                    }
462                }
463            }
464
465            // Simulate measurement counts
466            let occurrences = match self.config.solver {
467                AzureSolver::IonQ | AzureSolver::Quantinuum | AzureSolver::Rigetti => {
468                    // Quantum solvers return actual shot counts
469                    rng.random_range(1..=(shots / unique_solutions + 10))
470                }
471                _ => {
472                    // Classical solvers return occurrence frequencies
473                    1
474                }
475            };
476
477            results.push(SampleResult {
478                assignments,
479                energy,
480                occurrences,
481            });
482        }
483
484        // Sort by energy (best solutions first)
485        results.sort_by(|a, b| {
486            a.energy
487                .partial_cmp(&b.energy)
488                .unwrap_or(std::cmp::Ordering::Equal)
489        });
490
491        // Limit results to requested number
492        results.truncate(shots.min(100));
493
494        Ok(results)
495    }
496
497    fn run_hobo(
498        &self,
499        hobo: &(
500            Array<f64, scirs2_core::ndarray::IxDyn>,
501            HashMap<String, usize>,
502        ),
503        shots: usize,
504    ) -> SamplerResult<Vec<SampleResult>> {
505        use scirs2_core::ndarray::Ix2;
506
507        // For HOBO problems, convert to QUBO if possible
508        if hobo.0.ndim() <= 2 {
509            // If it's already 2D, just forward to run_qubo
510            let qubo_matrix = hobo.0.clone().into_dimensionality::<Ix2>().map_err(|e| {
511                SamplerError::InvalidParameter(format!(
512                    "Failed to convert HOBO to QUBO dimensionality: {e}"
513                ))
514            })?;
515            let qubo = (qubo_matrix, hobo.1.clone());
516            self.run_qubo(&qubo, shots)
517        } else {
518            // Azure Quantum doesn't directly support higher-order problems
519            Err(SamplerError::InvalidParameter(
520                "Azure Quantum doesn't support HOBO problems directly. Use a quadratization technique first.".to_string()
521            ))
522        }
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn test_azure_quantum_config() {
532        let config = AzureQuantumConfig::default();
533        assert_eq!(config.timeout, 300);
534        assert!(matches!(config.solver, AzureSolver::SimulatedAnnealing));
535    }
536
537    #[test]
538    fn test_azure_quantum_sampler_creation() {
539        let sampler =
540            AzureQuantumSampler::with_workspace("test-subscription", "test-rg", "test-workspace")
541                .with_solver(AzureSolver::ParallelTempering)
542                .with_timeout(600)
543                .with_param("temperature".to_string(), "0.5".to_string());
544
545        assert_eq!(sampler.config.subscription_id, "test-subscription");
546        assert_eq!(sampler.config.resource_group, "test-rg");
547        assert_eq!(sampler.config.workspace_name, "test-workspace");
548        assert_eq!(sampler.config.timeout, 600);
549        assert!(matches!(
550            sampler.config.solver,
551            AzureSolver::ParallelTempering
552        ));
553    }
554
555    #[test]
556    fn test_azure_solver_types() {
557        let solvers = [
558            AzureSolver::SimulatedAnnealing,
559            AzureSolver::ParallelTempering,
560            AzureSolver::TabuSearch,
561            AzureSolver::PopulationAnnealing,
562            AzureSolver::SubstrateMonteCarlo,
563            AzureSolver::IonQ,
564            AzureSolver::Quantinuum,
565            AzureSolver::Rigetti,
566        ];
567
568        assert_eq!(solvers.len(), 8);
569    }
570}