1use super::ast::{
4 ClassicalRef, Declaration, Expression, GateDefinition, Literal, Measurement, QasmGate,
5 QasmProgram, QasmRegister, QasmStatement, QubitRef,
6};
7use crate::builder::Circuit;
8use quantrs2_core::synthesis::decompose_single_qubit_zyz;
9use quantrs2_core::{gate::GateOp, qubit::QubitId};
10use scirs2_core::ndarray::Array2;
11use scirs2_core::Complex64;
12use std::collections::{HashMap, HashSet};
13use std::fmt::Write;
14use std::sync::Arc;
15use thiserror::Error;
16
17#[derive(Debug, Error)]
19pub enum ExportError {
20 #[error("Unsupported gate: {0}")]
21 UnsupportedGate(String),
22
23 #[error("Invalid circuit: {0}")]
24 InvalidCircuit(String),
25
26 #[error("Formatting error: {0}")]
27 FormattingError(#[from] std::fmt::Error),
28
29 #[error("Gate parameter error: {0}")]
30 ParameterError(String),
31
32 #[error("Cannot decompose custom gate '{gate}' for QASM export: {reason}")]
38 UndecomposableGate { gate: String, reason: String },
39}
40
41#[derive(Debug, Clone)]
43pub struct ExportOptions {
44 pub include_stdgates: bool,
46 pub decompose_custom: bool,
48 pub include_gate_comments: bool,
50 pub optimize: bool,
52 pub pretty_print: bool,
54}
55
56impl Default for ExportOptions {
57 fn default() -> Self {
58 Self {
59 include_stdgates: true,
60 decompose_custom: true,
61 include_gate_comments: false,
62 optimize: false,
63 pretty_print: true,
64 }
65 }
66}
67
68pub struct QasmExporter {
70 options: ExportOptions,
71 custom_gates: HashMap<String, GateInfo>,
73 qubit_usage: HashSet<usize>,
75 needs_classical_bits: bool,
77}
78
79#[derive(Clone)]
80struct GateInfo {
81 name: String,
82 num_qubits: usize,
83 num_params: usize,
84 matrix: Option<scirs2_core::ndarray::Array2<Complex64>>,
85}
86
87impl QasmExporter {
88 #[must_use]
90 pub fn new(options: ExportOptions) -> Self {
91 Self {
92 options,
93 custom_gates: HashMap::new(),
94 qubit_usage: HashSet::new(),
95 needs_classical_bits: false,
96 }
97 }
98
99 pub fn export<const N: usize>(&mut self, circuit: &Circuit<N>) -> Result<String, ExportError> {
101 self.analyze_circuit(circuit)?;
103
104 let program = self.generate_program(circuit)?;
106
107 Ok(program.to_string())
109 }
110
111 fn analyze_circuit<const N: usize>(&mut self, circuit: &Circuit<N>) -> Result<(), ExportError> {
113 self.qubit_usage.clear();
114 self.custom_gates.clear();
115 self.needs_classical_bits = false;
116
117 for gate in circuit.gates() {
119 for qubit in gate.qubits() {
121 self.qubit_usage.insert(qubit.id() as usize);
122 }
123
124 if !self.is_standard_gate(gate.as_ref()) {
126 self.register_custom_gate(gate.as_ref())?;
127 }
128
129 if gate.name().contains("measure") {
131 self.needs_classical_bits = true;
132 }
133 }
134
135 Ok(())
136 }
137
138 fn is_standard_gate(&self, gate: &dyn GateOp) -> bool {
140 let name = gate.name();
141 matches!(
142 name,
143 "I" | "X"
144 | "Y"
145 | "Z"
146 | "H"
147 | "S"
148 | "S†"
149 | "Sdg"
150 | "T"
151 | "T†"
152 | "Tdg"
153 | "√X"
154 | "√X†"
155 | "SX"
156 | "SXdg"
157 | "RX"
158 | "RY"
159 | "RZ"
160 | "P"
161 | "Phase"
162 | "U"
163 | "U1"
164 | "U2"
165 | "U3"
166 | "CX"
167 | "CNOT"
168 | "CY"
169 | "CZ"
170 | "CH"
171 | "CRX"
172 | "CRY"
173 | "CRZ"
174 | "CPhase"
175 | "SWAP"
176 | "iSWAP"
177 | "ECR"
178 | "DCX"
179 | "RXX"
180 | "RYY"
181 | "RZZ"
182 | "RZX"
183 | "CU"
184 | "CCX"
185 | "Toffoli"
186 | "Fredkin"
187 | "measure"
188 | "reset"
189 | "barrier"
190 )
191 }
192
193 fn register_custom_gate(&mut self, gate: &dyn GateOp) -> Result<(), ExportError> {
195 let name = self.gate_qasm_name(gate);
196
197 if !self.custom_gates.contains_key(&name) {
198 let num_qubits = gate.qubits().len();
199 let matrix = Self::gate_matrix(gate, num_qubits);
200
201 let info = GateInfo {
202 name: name.clone(),
203 num_qubits,
204 num_params: self.count_gate_params(gate),
205 matrix,
206 };
207
208 self.custom_gates.insert(name, info);
209 }
210
211 Ok(())
212 }
213
214 fn gate_matrix(gate: &dyn GateOp, num_qubits: usize) -> Option<Array2<Complex64>> {
220 let flat = gate.matrix().ok()?;
221 let dim = 1usize.checked_shl(num_qubits as u32)?;
222 if flat.len() != dim.checked_mul(dim)? {
223 return None;
224 }
225 Array2::from_shape_vec((dim, dim), flat).ok()
226 }
227
228 fn gate_qasm_name(&self, gate: &dyn GateOp) -> String {
230 let name = gate.name();
231 match name {
232 "I" => "id".to_string(),
233 "X" => "x".to_string(),
234 "Y" => "y".to_string(),
235 "Z" => "z".to_string(),
236 "H" => "h".to_string(),
237 "S" | "S†" => "s".to_string(),
238 "Sdg" => "sdg".to_string(),
239 "T" => "t".to_string(),
240 "T†" | "Tdg" => "tdg".to_string(),
241 "√X" | "SX" => "sx".to_string(),
242 "√X†" | "SXdg" => "sxdg".to_string(),
243 "RX" => "rx".to_string(),
244 "RY" => "ry".to_string(),
245 "RZ" => "rz".to_string(),
246 "P" | "Phase" => "p".to_string(),
247 "U" => "u".to_string(),
248 "CX" | "CNOT" => "cx".to_string(),
249 "CY" => "cy".to_string(),
250 "CZ" => "cz".to_string(),
251 "CH" => "ch".to_string(),
252 "CRX" => "crx".to_string(),
253 "CRY" => "cry".to_string(),
254 "CRZ" => "crz".to_string(),
255 "CPhase" => "cp".to_string(),
256 "SWAP" => "swap".to_string(),
257 "iSWAP" => "iswap".to_string(),
258 "ECR" => "ecr".to_string(),
259 "DCX" => "dcx".to_string(),
260 "RXX" => "rxx".to_string(),
261 "RYY" => "ryy".to_string(),
262 "RZZ" => "rzz".to_string(),
263 "RZX" => "rzx".to_string(),
264 "CCX" | "Toffoli" => "ccx".to_string(),
265 "Fredkin" => "cswap".to_string(),
266 _ => name.to_lowercase(),
267 }
268 }
269
270 fn count_gate_params(&self, gate: &dyn GateOp) -> usize {
272 let name = gate.name();
274 match name {
275 "RX" | "RY" | "RZ" | "P" | "Phase" | "U1" => 1,
276 "U2" => 2,
277 "U" | "U3" => 3,
278 "CRX" | "CRY" | "CRZ" | "CPhase" => 1,
279 "RXX" | "RYY" | "RZZ" | "RZX" => 1,
280 _ => 0,
281 }
282 }
283
284 fn generate_program<const N: usize>(
286 &self,
287 circuit: &Circuit<N>,
288 ) -> Result<QasmProgram, ExportError> {
289 let mut declarations = Vec::new();
290 let mut statements = Vec::new();
291
292 let max_qubit = self.qubit_usage.iter().max().copied().unwrap_or(0);
294 let num_qubits = max_qubit + 1;
295
296 declarations.push(Declaration::QuantumRegister(QasmRegister {
298 name: "q".to_string(),
299 size: num_qubits,
300 }));
301
302 if self.needs_classical_bits {
304 declarations.push(Declaration::ClassicalRegister(QasmRegister {
305 name: "c".to_string(),
306 size: num_qubits,
307 }));
308 }
309
310 if self.options.decompose_custom {
312 for gate_info in self.custom_gates.values() {
313 if let Some(def) = self.generate_gate_definition(gate_info)? {
314 declarations.push(Declaration::GateDefinition(def));
315 }
316 }
317 }
318
319 for gate in circuit.gates() {
321 statements.push(self.convert_gate(gate)?);
322 }
323
324 let includes = if self.options.include_stdgates {
326 vec!["stdgates.inc".to_string()]
327 } else {
328 vec![]
329 };
330
331 Ok(QasmProgram {
332 version: "3.0".to_string(),
333 includes,
334 declarations,
335 statements,
336 })
337 }
338
339 fn generate_gate_definition(
348 &self,
349 gate_info: &GateInfo,
350 ) -> Result<Option<GateDefinition>, ExportError> {
351 if gate_info.num_params > 0 {
356 return Err(ExportError::UndecomposableGate {
357 gate: gate_info.name.clone(),
358 reason: format!(
359 "parameterized custom gate with {} parameter(s); symbolic decomposition is not supported",
360 gate_info.num_params
361 ),
362 });
363 }
364
365 match gate_info.num_qubits {
366 0 => Err(ExportError::UndecomposableGate {
368 gate: gate_info.name.clone(),
369 reason: "gate acts on zero qubits".to_string(),
370 }),
371 1 => self.single_qubit_gate_definition(gate_info),
372 n => Err(ExportError::UndecomposableGate {
373 gate: gate_info.name.clone(),
374 reason: format!(
375 "no decomposition available for {n}-qubit custom gate (only single-qubit synthesis is implemented)"
376 ),
377 }),
378 }
379 }
380
381 fn single_qubit_gate_definition(
384 &self,
385 gate_info: &GateInfo,
386 ) -> Result<Option<GateDefinition>, ExportError> {
387 let matrix = gate_info
388 .matrix
389 .as_ref()
390 .ok_or_else(|| ExportError::UndecomposableGate {
391 gate: gate_info.name.clone(),
392 reason: "matrix representation unavailable".to_string(),
393 })?;
394
395 let decomp = decompose_single_qubit_zyz(&matrix.view()).map_err(|e| {
396 ExportError::UndecomposableGate {
397 gate: gate_info.name.clone(),
398 reason: format!("ZYZ decomposition failed: {e}"),
399 }
400 })?;
401
402 let qubit_arg = "qb".to_string();
413
414 let make_rotation = |name: &str, angle: f64| -> QasmStatement {
415 QasmStatement::Gate(QasmGate {
416 name: name.to_string(),
417 params: vec![Expression::Literal(Literal::Float(angle))],
418 qubits: vec![QubitRef::Register(qubit_arg.clone())],
419 control: None,
420 inverse: false,
421 power: None,
422 })
423 };
424
425 let body = vec![
426 make_rotation("rz", decomp.theta1),
427 make_rotation("ry", decomp.phi),
428 make_rotation("rz", decomp.theta2),
429 ];
430
431 Ok(Some(GateDefinition {
432 name: gate_info.name.clone(),
433 params: Vec::new(),
434 qubits: vec![qubit_arg],
435 body,
436 }))
437 }
438
439 fn convert_gate(
441 &self,
442 gate: &Arc<dyn GateOp + Send + Sync>,
443 ) -> Result<QasmStatement, ExportError> {
444 let gate_name = gate.name();
445
446 match gate_name {
447 "measure" => {
448 let qubits: Vec<QubitRef> = gate
450 .qubits()
451 .iter()
452 .map(|q| QubitRef::Single {
453 register: "q".to_string(),
454 index: q.id() as usize,
455 })
456 .collect();
457
458 let targets: Vec<ClassicalRef> = gate
459 .qubits()
460 .iter()
461 .map(|q| ClassicalRef::Single {
462 register: "c".to_string(),
463 index: q.id() as usize,
464 })
465 .collect();
466
467 Ok(QasmStatement::Measure(Measurement { qubits, targets }))
468 }
469 "reset" => {
470 let qubits: Vec<QubitRef> = gate
471 .qubits()
472 .iter()
473 .map(|q| QubitRef::Single {
474 register: "q".to_string(),
475 index: q.id() as usize,
476 })
477 .collect();
478
479 Ok(QasmStatement::Reset(qubits))
480 }
481 "barrier" => {
482 let qubits: Vec<QubitRef> = gate
483 .qubits()
484 .iter()
485 .map(|q| QubitRef::Single {
486 register: "q".to_string(),
487 index: q.id() as usize,
488 })
489 .collect();
490
491 Ok(QasmStatement::Barrier(qubits))
492 }
493 _ => {
494 let name = self.gate_qasm_name(gate.as_ref());
496
497 let qubits: Vec<QubitRef> = gate
498 .qubits()
499 .iter()
500 .map(|q| QubitRef::Single {
501 register: "q".to_string(),
502 index: q.id() as usize,
503 })
504 .collect();
505
506 let params = self.extract_gate_params(gate.as_ref())?;
508
509 Ok(QasmStatement::Gate(QasmGate {
510 name,
511 params,
512 qubits,
513 control: None,
514 inverse: false,
515 power: None,
516 }))
517 }
518 }
519 }
520
521 fn extract_gate_params(&self, gate: &dyn GateOp) -> Result<Vec<Expression>, ExportError> {
523 use quantrs2_core::gate::multi::{CRX, CRY, CRZ};
524 use quantrs2_core::gate::single::{RotationX, RotationY, RotationZ};
525 use std::any::Any;
526
527 let any_gate = gate.as_any();
528
529 if let Some(rx) = any_gate.downcast_ref::<RotationX>() {
531 return Ok(vec![Expression::Literal(Literal::Float(rx.theta))]);
532 }
533 if let Some(ry) = any_gate.downcast_ref::<RotationY>() {
534 return Ok(vec![Expression::Literal(Literal::Float(ry.theta))]);
535 }
536 if let Some(rz) = any_gate.downcast_ref::<RotationZ>() {
537 return Ok(vec![Expression::Literal(Literal::Float(rz.theta))]);
538 }
539
540 if let Some(crx) = any_gate.downcast_ref::<CRX>() {
542 return Ok(vec![Expression::Literal(Literal::Float(crx.theta))]);
543 }
544 if let Some(cry) = any_gate.downcast_ref::<CRY>() {
545 return Ok(vec![Expression::Literal(Literal::Float(cry.theta))]);
546 }
547 if let Some(crz) = any_gate.downcast_ref::<CRZ>() {
548 return Ok(vec![Expression::Literal(Literal::Float(crz.theta))]);
549 }
550
551 Ok(vec![])
553 }
554}
555
556pub fn export_qasm3<const N: usize>(circuit: &Circuit<N>) -> Result<String, ExportError> {
558 let mut exporter = QasmExporter::new(ExportOptions::default());
559 exporter.export(circuit)
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use quantrs2_core::error::QuantRS2Result;
566 use quantrs2_core::gate::multi::CNOT;
567 use quantrs2_core::gate::single::{Hadamard, PauliX};
568 use quantrs2_core::qubit::QubitId;
569 use std::any::Any;
570
571 #[derive(Debug, Clone)]
575 struct CustomHadamard {
576 target: QubitId,
577 }
578
579 impl GateOp for CustomHadamard {
580 fn name(&self) -> &'static str {
581 "myhad"
582 }
583 fn qubits(&self) -> Vec<QubitId> {
584 vec![self.target]
585 }
586 fn matrix(&self) -> QuantRS2Result<Vec<Complex64>> {
587 let s = 1.0 / 2.0_f64.sqrt();
588 Ok(vec![
589 Complex64::new(s, 0.0),
590 Complex64::new(s, 0.0),
591 Complex64::new(s, 0.0),
592 Complex64::new(-s, 0.0),
593 ])
594 }
595 fn as_any(&self) -> &dyn Any {
596 self
597 }
598 fn clone_gate(&self) -> Box<dyn GateOp> {
599 Box::new(self.clone())
600 }
601 }
602
603 #[derive(Debug, Clone)]
607 struct CustomTwoQubit {
608 a: QubitId,
609 b: QubitId,
610 }
611
612 impl GateOp for CustomTwoQubit {
613 fn name(&self) -> &'static str {
614 "mytwo"
615 }
616 fn qubits(&self) -> Vec<QubitId> {
617 vec![self.a, self.b]
618 }
619 fn matrix(&self) -> QuantRS2Result<Vec<Complex64>> {
620 let mut m = vec![Complex64::new(0.0, 0.0); 16];
621 for i in 0..4 {
622 m[i * 4 + i] = Complex64::new(1.0, 0.0);
623 }
624 Ok(m)
625 }
626 fn as_any(&self) -> &dyn Any {
627 self
628 }
629 fn clone_gate(&self) -> Box<dyn GateOp> {
630 Box::new(self.clone())
631 }
632 }
633
634 #[test]
635 fn test_export_custom_single_qubit_gate_emits_definition() {
636 let mut circuit = Circuit::<1>::new();
637 circuit
638 .add_gate(CustomHadamard { target: QubitId(0) })
639 .expect("adding custom gate should succeed");
640
641 let qasm = export_qasm3(&circuit)
642 .expect("export should synthesize a definition for a single-qubit custom gate");
643
644 assert!(
647 qasm.contains("gate myhad"),
648 "missing custom gate definition: {qasm}"
649 );
650 assert!(qasm.contains("rz("), "definition body missing rz: {qasm}");
651 assert!(qasm.contains("ry("), "definition body missing ry: {qasm}");
652 assert!(
653 qasm.contains("myhad qb") || qasm.contains("myhad q[0]"),
654 "custom gate not applied: {qasm}"
655 );
656 }
657
658 #[test]
659 fn test_export_custom_multi_qubit_gate_errors_honestly() {
660 let mut circuit = Circuit::<2>::new();
661 circuit
662 .add_gate(CustomTwoQubit {
663 a: QubitId(0),
664 b: QubitId(1),
665 })
666 .expect("adding custom two-qubit gate should succeed");
667
668 let result = export_qasm3(&circuit);
669 assert!(
671 matches!(result, Err(ExportError::UndecomposableGate { ref gate, .. }) if gate == "mytwo"),
672 "expected UndecomposableGate error for two-qubit custom gate, got: {result:?}"
673 );
674 }
675
676 #[test]
677 fn test_custom_gate_definition_is_skipped_when_disabled() {
678 let mut circuit = Circuit::<2>::new();
681 circuit
682 .add_gate(CustomTwoQubit {
683 a: QubitId(0),
684 b: QubitId(1),
685 })
686 .expect("adding custom two-qubit gate should succeed");
687
688 let options = ExportOptions {
689 decompose_custom: false,
690 ..ExportOptions::default()
691 };
692 let mut exporter = QasmExporter::new(options);
693 let qasm = exporter
694 .export(&circuit)
695 .expect("export without decomposition should not error");
696 assert!(
697 !qasm.contains("gate mytwo"),
698 "unexpected definition: {qasm}"
699 );
700 }
701
702 #[test]
703 fn test_export_simple_circuit() {
704 let mut circuit = Circuit::<2>::new();
705 circuit
706 .add_gate(Hadamard { target: QubitId(0) })
707 .expect("adding Hadamard gate should succeed");
708 circuit
709 .add_gate(CNOT {
710 control: QubitId(0),
711 target: QubitId(1),
712 })
713 .expect("adding CNOT gate should succeed");
714
715 let result = export_qasm3(&circuit);
716 assert!(result.is_ok());
717
718 let qasm = result.expect("export_qasm3 should succeed for valid circuit");
719 assert!(qasm.contains("OPENQASM 3.0"));
720 assert!(qasm.contains("qubit[2] q"));
721 assert!(qasm.contains("h q[0]"));
722 assert!(qasm.contains("cx q[0], q[1]"));
723 }
724
725 #[test]
726 fn test_export_with_measurements() {
727 let mut circuit = Circuit::<2>::new();
728 circuit
729 .add_gate(Hadamard { target: QubitId(0) })
730 .expect("adding Hadamard gate should succeed");
731 let result = export_qasm3(&circuit);
734 assert!(result.is_ok());
735
736 let qasm = result.expect("export_qasm3 should succeed for measurement test");
737 assert!(qasm.contains("OPENQASM 3.0"));
739 }
740}