Skip to main content

quantrs2_device/
transpiler.rs

1//! Circuit transpilation utilities for hardware device connectivity.
2//!
3//! Converts QuantRS2 circuits to QASM and transpiles them to match device
4//! qubit connectivity graphs, basis gate sets, and physical qubit mappings.
5
6use quantrs2_circuit::prelude::Circuit;
7use quantrs2_core::gate::single::{RotationX, RotationY, RotationZ};
8use quantrs2_core::prelude::QubitId;
9use std::collections::{HashMap, VecDeque};
10
11use crate::DeviceError;
12use crate::DeviceResult;
13
14/// QASM string representation of a quantum circuit
15#[derive(Debug, Clone)]
16pub struct QasmCircuit {
17    /// QASM content
18    pub content: String,
19    /// Number of qubits in the circuit
20    pub qubit_count: usize,
21    /// Number of classical bits
22    pub cbit_count: usize,
23    /// Gate counts by type
24    pub gate_counts: HashMap<String, usize>,
25}
26
27/// Circuit transpiler that converts between different quantum circuit representations.
28///
29/// Converts QuantRS2 [`Circuit`] instances to OpenQASM 2.0 text format and
30/// performs physical qubit remapping for hardware connectivity constraints.
31///
32/// # Examples
33///
34/// ```rust
35/// use quantrs2_device::transpiler::CircuitTranspiler;
36/// use quantrs2_circuit::builder::Circuit;
37///
38/// let mut circ: Circuit<2> = Circuit::new();
39/// circ.h(0).expect("h").cnot(0, 1).expect("cnot");
40///
41/// let qasm = CircuitTranspiler::circuit_to_qasm(&circ, None)
42///     .expect("transpile failed");
43/// assert!(qasm.content.contains("OPENQASM 2.0"));
44/// assert_eq!(qasm.qubit_count, 2);
45/// ```
46pub struct CircuitTranspiler;
47
48impl CircuitTranspiler {
49    /// Convert a Quantrs circuit to OpenQASM 2.0 format
50    pub fn circuit_to_qasm<const N: usize>(
51        circuit: &Circuit<N>,
52        qubit_mapping: Option<&HashMap<usize, usize>>,
53    ) -> DeviceResult<QasmCircuit> {
54        // Start QASM generation
55        let mut qasm = String::from("OPENQASM 2.0;\ninclude \"qelib1.inc\";\n\n");
56
57        // Define the quantum and classical registers
58        use std::fmt::Write;
59        let _ = writeln!(qasm, "qreg q[{N}];");
60        let _ = writeln!(qasm, "creg c[{N}];");
61
62        // Track gate counts
63        let mut gate_counts = HashMap::new();
64
65        // Process each gate in the circuit
66        for gate in circuit.gates() {
67            let gate_qasm = match gate.name() {
68                "X" => {
69                    if gate.qubits().len() != 1 {
70                        continue;
71                    }
72                    let qubit = gate.qubits()[0];
73                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
74                    *gate_counts.entry("x".to_string()).or_insert(0) += 1;
75                    format!("x q[{q}];")
76                }
77                "Y" => {
78                    if gate.qubits().len() != 1 {
79                        continue;
80                    }
81                    let qubit = gate.qubits()[0];
82                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
83                    *gate_counts.entry("y".to_string()).or_insert(0) += 1;
84                    format!("y q[{q}];")
85                }
86                "Z" => {
87                    if gate.qubits().len() != 1 {
88                        continue;
89                    }
90                    let qubit = gate.qubits()[0];
91                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
92                    *gate_counts.entry("z".to_string()).or_insert(0) += 1;
93                    format!("z q[{q}];")
94                }
95                "H" => {
96                    if gate.qubits().len() != 1 {
97                        continue;
98                    }
99                    let qubit = gate.qubits()[0];
100                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
101                    *gate_counts.entry("h".to_string()).or_insert(0) += 1;
102                    format!("h q[{q}];")
103                }
104                "S" => {
105                    if gate.qubits().len() != 1 {
106                        continue;
107                    }
108                    let qubit = gate.qubits()[0];
109                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
110                    *gate_counts.entry("s".to_string()).or_insert(0) += 1;
111                    format!("s q[{q}];")
112                }
113                "S†" => {
114                    if gate.qubits().len() != 1 {
115                        continue;
116                    }
117                    let qubit = gate.qubits()[0];
118                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
119                    *gate_counts.entry("sdg".to_string()).or_insert(0) += 1;
120                    format!("sdg q[{q}];")
121                }
122                "T" => {
123                    if gate.qubits().len() != 1 {
124                        continue;
125                    }
126                    let qubit = gate.qubits()[0];
127                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
128                    *gate_counts.entry("t".to_string()).or_insert(0) += 1;
129                    format!("t q[{q}];")
130                }
131                "T†" => {
132                    if gate.qubits().len() != 1 {
133                        continue;
134                    }
135                    let qubit = gate.qubits()[0];
136                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
137                    *gate_counts.entry("tdg".to_string()).or_insert(0) += 1;
138                    format!("tdg q[{q}];")
139                }
140                "CNOT" => {
141                    if gate.qubits().len() != 2 {
142                        continue;
143                    }
144                    let control = gate.qubits()[0];
145                    let target = gate.qubits()[1];
146                    let c = map_qubit(control.id() as usize, &qubit_mapping);
147                    let t = map_qubit(target.id() as usize, &qubit_mapping);
148                    *gate_counts.entry("cx".to_string()).or_insert(0) += 1;
149                    format!("cx q[{c}], q[{t}];")
150                }
151                "CZ" => {
152                    if gate.qubits().len() != 2 {
153                        continue;
154                    }
155                    let control = gate.qubits()[0];
156                    let target = gate.qubits()[1];
157                    let c = map_qubit(control.id() as usize, &qubit_mapping);
158                    let t = map_qubit(target.id() as usize, &qubit_mapping);
159                    *gate_counts.entry("cz".to_string()).or_insert(0) += 1;
160                    format!("cz q[{c}], q[{t}];")
161                }
162                "SWAP" => {
163                    if gate.qubits().len() != 2 {
164                        continue;
165                    }
166                    let q1 = gate.qubits()[0];
167                    let q2 = gate.qubits()[1];
168                    let q1_mapped = map_qubit(q1.id() as usize, &qubit_mapping);
169                    let q2_mapped = map_qubit(q2.id() as usize, &qubit_mapping);
170                    *gate_counts.entry("swap".to_string()).or_insert(0) += 1;
171                    format!("swap q[{q1_mapped}], q[{q2_mapped}];")
172                }
173                "RX" => {
174                    if gate.qubits().len() != 1 {
175                        continue;
176                    }
177                    let qubit = gate.qubits()[0];
178                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
179                    let theta = gate
180                        .as_any()
181                        .downcast_ref::<RotationX>()
182                        .map(|g| g.theta)
183                        .unwrap_or(0.0);
184                    *gate_counts.entry("rx".to_string()).or_insert(0) += 1;
185                    format!("rx({theta}) q[{q}];")
186                }
187                "RY" => {
188                    if gate.qubits().len() != 1 {
189                        continue;
190                    }
191                    let qubit = gate.qubits()[0];
192                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
193                    let theta = gate
194                        .as_any()
195                        .downcast_ref::<RotationY>()
196                        .map(|g| g.theta)
197                        .unwrap_or(0.0);
198                    *gate_counts.entry("ry".to_string()).or_insert(0) += 1;
199                    format!("ry({theta}) q[{q}];")
200                }
201                "RZ" => {
202                    if gate.qubits().len() != 1 {
203                        continue;
204                    }
205                    let qubit = gate.qubits()[0];
206                    let q = map_qubit(qubit.id() as usize, &qubit_mapping);
207                    let theta = gate
208                        .as_any()
209                        .downcast_ref::<RotationZ>()
210                        .map(|g| g.theta)
211                        .unwrap_or(0.0);
212                    *gate_counts.entry("rz".to_string()).or_insert(0) += 1;
213                    format!("rz({theta}) q[{q}];")
214                }
215                _ => {
216                    // For now, return an error for unsupported gates
217                    return Err(DeviceError::CircuitConversion(format!(
218                        "Unsupported gate type for QASM conversion: {}",
219                        gate.name()
220                    )));
221                }
222            };
223
224            let _ = writeln!(qasm, "{gate_qasm}");
225        }
226
227        Ok(QasmCircuit {
228            content: qasm,
229            qubit_count: N,
230            cbit_count: N, // Assuming same number of classical bits as qubits
231            gate_counts,
232        })
233    }
234
235    /// Find the optimal qubit mapping for a circuit based on the device's coupling map
236    pub fn optimize_qubit_mapping<const N: usize>(
237        circuit: &Circuit<N>,
238        coupling_map: &[(usize, usize)],
239    ) -> HashMap<usize, usize> {
240        // This is a simplified implementation of a qubit mapping algorithm
241        // In a production system, this would use more sophisticated algorithms
242        // like simulated annealing or subgraph isomorphism
243
244        // Count the interactions between each pair of qubits
245        let mut qubit_interactions = HashMap::new();
246
247        for gate in circuit.gates() {
248            let qubits = gate.qubits();
249            if qubits.len() == 2 {
250                let q1 = qubits[0].id() as usize;
251                let q2 = qubits[1].id() as usize;
252                *qubit_interactions.entry((q1, q2)).or_insert(0) += 1;
253            }
254        }
255
256        // Sort qubit pairs by interaction frequency
257        let mut pairs: Vec<_> = qubit_interactions.iter().collect();
258        pairs.sort_by(|a, b| b.1.cmp(a.1));
259
260        // Create a mapping from circuit qubits to device qubits
261        let mut mapping = HashMap::new();
262        let mut used_device_qubits = std::collections::HashSet::new();
263
264        // First, map qubit pairs with the most interactions to connected device qubits
265        for (&(q1, q2), _) in &pairs {
266            if mapping.contains_key(&q1) && mapping.contains_key(&q2) {
267                continue;
268            }
269
270            // Find an available connected pair on the device
271            for &(dev_q1, dev_q2) in coupling_map {
272                if (!mapping.contains_key(&q1) || mapping[&q1] == dev_q1)
273                    && (!mapping.contains_key(&q2) || mapping[&q2] == dev_q2)
274                    && !used_device_qubits.contains(&dev_q1)
275                    && !used_device_qubits.contains(&dev_q2)
276                {
277                    // Map this pair
278                    if let std::collections::hash_map::Entry::Vacant(e) = mapping.entry(q1) {
279                        e.insert(dev_q1);
280                        used_device_qubits.insert(dev_q1);
281                    }
282                    if let std::collections::hash_map::Entry::Vacant(e) = mapping.entry(q2) {
283                        e.insert(dev_q2);
284                        used_device_qubits.insert(dev_q2);
285                    }
286                    break;
287                }
288
289                if (!mapping.contains_key(&q1) || mapping[&q1] == dev_q2)
290                    && (!mapping.contains_key(&q2) || mapping[&q2] == dev_q1)
291                    && !used_device_qubits.contains(&dev_q1)
292                    && !used_device_qubits.contains(&dev_q2)
293                {
294                    // Map this pair (reversed)
295                    if let std::collections::hash_map::Entry::Vacant(e) = mapping.entry(q1) {
296                        e.insert(dev_q2);
297                        used_device_qubits.insert(dev_q2);
298                    }
299                    if let std::collections::hash_map::Entry::Vacant(e) = mapping.entry(q2) {
300                        e.insert(dev_q1);
301                        used_device_qubits.insert(dev_q1);
302                    }
303                    break;
304                }
305            }
306        }
307
308        // For any unmapped qubits, assign them to any unused device qubits
309        for q in 0..N {
310            if !mapping.contains_key(&q) {
311                // Find any unused device qubit
312                for dev_q in 0..N {
313                    if used_device_qubits.insert(dev_q) {
314                        mapping.insert(q, dev_q);
315                        break;
316                    }
317                }
318            }
319        }
320
321        // If there are still unmapped qubits, just use identity mapping for simplicity
322        // In a real implementation, this would be more sophisticated
323        for q in 0..N {
324            mapping.entry(q).or_insert(q);
325        }
326
327        mapping
328    }
329
330    /// Transpile a circuit to adapt to device constraints
331    pub fn transpile_circuit<const N: usize>(
332        circuit: &Circuit<N>,
333        coupling_map: &[(usize, usize)],
334    ) -> DeviceResult<(Circuit<N>, HashMap<usize, usize>)> {
335        // First, determine the optimal qubit mapping
336        let mapping = Self::optimize_qubit_mapping(circuit, coupling_map);
337
338        // Create a new circuit with the same number of qubits
339        let mut new_circuit = Circuit::<N>::new();
340
341        // Track which gates have been used for mapping
342        let mut used_gates = vec![false; circuit.gates().len()];
343
344        // Process each gate in the original circuit
345        for (i, gate) in circuit.gates().iter().enumerate() {
346            if used_gates[i] {
347                continue;
348            }
349
350            // Process gate based on its name and properties
351            let qubits = gate.qubits();
352            match gate.name() {
353                "CNOT" => {
354                    if qubits.len() != 2 {
355                        continue;
356                    }
357                    let control = qubits[0];
358                    let target = qubits[1];
359                    let c = control.id() as usize;
360                    let t = target.id() as usize;
361                    let mapped_c = mapping[&c];
362                    let mapped_t = mapping[&t];
363
364                    // Check if the mapped qubits are connected in the coupling map
365                    let directly_connected = coupling_map.contains(&(mapped_c, mapped_t))
366                        || coupling_map.contains(&(mapped_t, mapped_c));
367
368                    if directly_connected {
369                        // If directly connected, just add the gate with mapped qubits
370                        if coupling_map.contains(&(mapped_c, mapped_t)) {
371                            let _ = new_circuit
372                                .cnot(QubitId::new(mapped_c as u32), QubitId::new(mapped_t as u32));
373                        } else {
374                            // If connected in reverse direction, we need to use SWAP tricks
375                            // H - CNOT - H sequence to reverse the control and target
376                            let _ = new_circuit.h(QubitId::new(mapped_c as u32));
377                            let _ = new_circuit.h(QubitId::new(mapped_t as u32));
378                            let _ = new_circuit
379                                .cnot(QubitId::new(mapped_t as u32), QubitId::new(mapped_c as u32));
380                            let _ = new_circuit.h(QubitId::new(mapped_c as u32));
381                            let _ = new_circuit.h(QubitId::new(mapped_t as u32));
382                        }
383                    } else {
384                        // If not directly connected, we need to find a path and insert SWAP gates
385                        // This is a simplified version - in a real implementation this would
386                        // be more sophisticated
387                        let path = find_shortest_path(mapped_c, mapped_t, coupling_map);
388                        if path.is_empty() {
389                            return Err(DeviceError::CircuitConversion(format!(
390                                "No path found between qubits {mapped_c} and {mapped_t}"
391                            )));
392                        }
393
394                        // Apply SWAP gates along the path to bring qubits next to each other
395                        for i in 0..path.len() - 2 {
396                            let _ = new_circuit.swap(
397                                QubitId::new(path[i] as u32),
398                                QubitId::new(path[i + 1] as u32),
399                            );
400                        }
401
402                        // Now the control and target are adjacent, so apply the CNOT
403                        let final_c = path[path.len() - 2];
404                        let final_t = path[path.len() - 1];
405                        let _ = new_circuit
406                            .cnot(QubitId::new(final_c as u32), QubitId::new(final_t as u32));
407
408                        // Undo the SWAP gates in reverse order
409                        for i in (0..path.len() - 2).rev() {
410                            let _ = new_circuit.swap(
411                                QubitId::new(path[i] as u32),
412                                QubitId::new(path[i + 1] as u32),
413                            );
414                        }
415                    }
416
417                    used_gates[i] = true;
418                }
419                // Handle other gate types - most are straightforward as they're single-qubit
420                "X" => {
421                    if qubits.len() != 1 {
422                        continue;
423                    }
424                    let qubit = qubits[0];
425                    let q = qubit.id() as usize;
426                    let mapped_q = mapping[&q];
427                    let _ = new_circuit.x(QubitId::new(mapped_q as u32));
428                    used_gates[i] = true;
429                }
430                "Y" => {
431                    if qubits.len() != 1 {
432                        continue;
433                    }
434                    let qubit = qubits[0];
435                    let q = qubit.id() as usize;
436                    let mapped_q = mapping[&q];
437                    let _ = new_circuit.y(QubitId::new(mapped_q as u32));
438                    used_gates[i] = true;
439                }
440                "Z" => {
441                    if qubits.len() != 1 {
442                        continue;
443                    }
444                    let qubit = qubits[0];
445                    let q = qubit.id() as usize;
446                    let mapped_q = mapping[&q];
447                    let _ = new_circuit.z(QubitId::new(mapped_q as u32));
448                    used_gates[i] = true;
449                }
450                "H" => {
451                    if qubits.len() != 1 {
452                        continue;
453                    }
454                    let qubit = qubits[0];
455                    let q = qubit.id() as usize;
456                    let mapped_q = mapping[&q];
457                    let _ = new_circuit.h(QubitId::new(mapped_q as u32));
458                    used_gates[i] = true;
459                }
460                // Add other gate types as needed
461                _ => {
462                    // For now, return an error for unsupported gates
463                    return Err(DeviceError::CircuitConversion(format!(
464                        "Unsupported gate type for transpilation: {}",
465                        gate.name()
466                    )));
467                }
468            }
469        }
470
471        Ok((new_circuit, mapping))
472    }
473}
474
475// Helper function to map a qubit index based on a mapping
476fn map_qubit(qubit: usize, mapping: &Option<&HashMap<usize, usize>>) -> usize {
477    match mapping {
478        Some(map) => *map.get(&qubit).unwrap_or(&qubit),
479        None => qubit,
480    }
481}
482
483// Helper function to find the shortest path between two qubits in the coupling map
484fn find_shortest_path(start: usize, end: usize, coupling_map: &[(usize, usize)]) -> Vec<usize> {
485    if start == end {
486        return vec![start];
487    }
488
489    // Create an adjacency list from the coupling map
490    let mut adj_list = HashMap::new();
491    for &(a, b) in coupling_map {
492        adj_list.entry(a).or_insert_with(Vec::new).push(b);
493        adj_list.entry(b).or_insert_with(Vec::new).push(a);
494    }
495
496    // Perform BFS to find the shortest path
497    let mut queue = VecDeque::new();
498    let mut visited = std::collections::HashSet::new();
499    let mut parent = HashMap::new();
500
501    queue.push_back(start);
502    visited.insert(start);
503
504    while let Some(node) = queue.pop_front() {
505        if node == end {
506            // Reconstruct the path
507            let mut path = Vec::new();
508            let mut current = node;
509
510            while let Some(&p) = parent.get(&current) {
511                path.push(current);
512                current = p;
513                if current == start {
514                    path.push(current);
515                    path.reverse();
516                    return path;
517                }
518            }
519        }
520
521        if let Some(neighbors) = adj_list.get(&node) {
522            for &neighbor in neighbors {
523                if visited.insert(neighbor) {
524                    queue.push_back(neighbor);
525                    parent.insert(neighbor, node);
526                }
527            }
528        }
529    }
530
531    // No path found
532    Vec::new()
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538    use quantrs2_circuit::builder::Circuit;
539
540    #[test]
541    fn test_qasm_export_rotation_angles() {
542        // Build a 3-qubit circuit with RX(0.5), RY(1.0), RZ(1.5)
543        let mut circ: Circuit<3> = Circuit::new();
544        circ.rx(0u32, 0.5).expect("rx gate");
545        circ.ry(1u32, 1.0).expect("ry gate");
546        circ.rz(2u32, 1.5).expect("rz gate");
547
548        let qasm = CircuitTranspiler::circuit_to_qasm(&circ, None)
549            .expect("circuit_to_qasm should succeed");
550
551        assert!(
552            qasm.content.contains("rx(0.5)"),
553            "QASM should contain rx(0.5), got: {}",
554            qasm.content
555        );
556        // Rust formats 1.0_f64 as "1" via Display
557        assert!(
558            qasm.content.contains("ry(1)") || qasm.content.contains("ry(1.0)"),
559            "QASM should contain ry(1) or ry(1.0), got: {}",
560            qasm.content
561        );
562        assert!(
563            qasm.content.contains("rz(1.5)"),
564            "QASM should contain rz(1.5), got: {}",
565            qasm.content
566        );
567
568        // Ensure old placeholder values are absent — "rx(0) " (with trailing space) was the bug
569        assert!(
570            !qasm.content.contains("rx(0) "),
571            "QASM must not contain hardcoded placeholder rx(0), got: {}",
572            qasm.content
573        );
574        assert!(
575            !qasm.content.contains("ry(0) "),
576            "QASM must not contain hardcoded placeholder ry(0), got: {}",
577            qasm.content
578        );
579        assert!(
580            !qasm.content.contains("rz(0) "),
581            "QASM must not contain hardcoded placeholder rz(0), got: {}",
582            qasm.content
583        );
584    }
585}