1use crate::circuit::{
22 Instruction, Parameter, ParameterValue, Qubit, StandardGate, ValueInstruction, ValueOperation,
23};
24use crate::compile::CompilerError;
25use crate::compile::transform::decompose::unitary::TwoQubitSynthesisTarget;
26use crate::compile::transform::target_basis::TargetBasisSignature;
27use ndarray::Array2;
28use num_complex::Complex64;
29use smallvec::SmallVec;
30use std::collections::HashMap;
31
32#[derive(Debug, Clone, Default)]
34pub struct DecompositionRuleCache {
35 mc_gate_rules: HashMap<McGateRuleKey, DecompositionRule>,
36 numeric_unitary_rules: HashMap<NumericUnitaryRuleKey, NumericUnitaryRule>,
37 stats: DecompositionRuleStats,
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42pub struct DecompositionRuleStats {
43 pub hits: usize,
44 pub misses: usize,
45 pub inserts: usize,
46}
47
48impl DecompositionRuleCache {
49 pub fn instantiate_mc_gate(
54 &mut self,
55 request: McGateRuleRequest<'_>,
56 controls: &[Qubit],
57 targets: &[Qubit],
58 ancillas: &[Qubit],
59 ) -> Result<Option<Vec<ValueOperation>>, CompilerError> {
60 let key = McGateRuleKey::from_request(request);
61 if let Some(rule) = self.mc_gate_rules.get(&key) {
62 self.stats.hits += 1;
63 return Ok(Some(rule.instantiate(controls, targets, ancillas)?));
64 }
65 self.stats.misses += 1;
66 Ok(None)
67 }
68
69 pub fn insert_mc_gate(
75 &mut self,
76 request: McGateRuleRequest<'_>,
77 controls: &[Qubit],
78 targets: &[Qubit],
79 ancillas: &[Qubit],
80 operations: &[ValueOperation],
81 ) -> Result<(), CompilerError> {
82 let key = McGateRuleKey::from_request(request);
83 let rule = DecompositionRule::from_operations(controls, targets, ancillas, operations)?;
84 self.mc_gate_rules.insert(key, rule);
85 self.stats.inserts += 1;
86 Ok(())
87 }
88
89 pub const fn stats(&self) -> DecompositionRuleStats {
91 self.stats
92 }
93
94 pub fn instantiate_numeric_unitary(
98 &mut self,
99 request: NumericUnitaryRuleRequest<'_>,
100 qubits: &[Qubit],
101 ) -> Result<Option<(Vec<ValueOperation>, f64)>, CompilerError> {
102 let key = NumericUnitaryRuleKey::from_request(request);
103 if let Some(rule) = self.numeric_unitary_rules.get(&key) {
104 self.stats.hits += 1;
105 return Ok(Some((
106 rule.rule.instantiate(&[], qubits, &[])?,
107 rule.phase_delta,
108 )));
109 }
110 self.stats.misses += 1;
111 Ok(None)
112 }
113
114 pub fn insert_numeric_unitary(
119 &mut self,
120 request: NumericUnitaryRuleRequest<'_>,
121 qubits: &[Qubit],
122 operations: &[ValueOperation],
123 phase_delta: f64,
124 ) -> Result<(), CompilerError> {
125 let key = NumericUnitaryRuleKey::from_request(request);
126 let rule = DecompositionRule::from_operations(&[], qubits, &[], operations)?;
127 self.numeric_unitary_rules
128 .insert(key, NumericUnitaryRule { rule, phase_delta });
129 self.stats.inserts += 1;
130 Ok(())
131 }
132}
133
134#[derive(Debug, Clone, Copy)]
136pub struct McGateRuleRequest<'a> {
137 pub gate: StandardGate,
138 pub control_count: usize,
139 pub target_count: usize,
140 pub params: &'a [ParameterValue],
141 pub resource: ResourceSignature,
142}
143
144#[derive(Debug, Clone)]
146pub struct NumericUnitaryRuleRequest<'a> {
147 pub num_qubits: u16,
148 pub matrix: &'a Array2<Complex64>,
149 pub synthesis_key: NumericUnitarySynthesisKey,
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Hash)]
158pub enum NumericUnitarySynthesisKey {
159 OneQubit,
160 TwoQubit {
161 target_basis: Option<TargetBasisSignature>,
162 fallback_pauli: bool,
163 },
164}
165
166impl NumericUnitarySynthesisKey {
167 pub const fn one_qubit() -> Self {
168 Self::OneQubit
169 }
170
171 pub(crate) fn two_qubit(target: &TwoQubitSynthesisTarget) -> Self {
172 Self::TwoQubit {
173 target_basis: target.cache_signature(),
174 fallback_pauli: target.fallback_pauli(),
175 }
176 }
177}
178
179#[derive(Debug, Clone)]
180struct NumericUnitaryRule {
181 rule: DecompositionRule,
182 phase_delta: f64,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Hash)]
186struct NumericUnitaryRuleKey {
187 num_qubits: u16,
188 rows: usize,
189 cols: usize,
190 matrix: Vec<(u64, u64)>,
191 synthesis_key: NumericUnitarySynthesisKey,
192}
193
194impl NumericUnitaryRuleKey {
195 fn from_request(request: NumericUnitaryRuleRequest<'_>) -> Self {
196 Self {
197 num_qubits: request.num_qubits,
198 rows: request.matrix.nrows(),
199 cols: request.matrix.ncols(),
200 matrix: request
201 .matrix
202 .iter()
203 .map(|value| (value.re.to_bits(), value.im.to_bits()))
204 .collect(),
205 synthesis_key: request.synthesis_key,
206 }
207 }
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Hash)]
211struct McGateRuleKey {
212 gate: StandardGate,
213 control_count: usize,
214 target_count: usize,
215 params: Vec<ParameterValueKey>,
216 resource: ResourceSignature,
217}
218
219impl McGateRuleKey {
220 fn from_request(request: McGateRuleRequest<'_>) -> Self {
221 Self {
222 gate: request.gate,
223 control_count: request.control_count,
224 target_count: request.target_count,
225 params: request.params.iter().map(ParameterValueKey::from).collect(),
226 resource: request.resource,
227 }
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232enum ParameterValueKey {
233 Fixed(u64),
234 Param(Parameter),
235}
236
237impl From<&ParameterValue> for ParameterValueKey {
238 fn from(value: &ParameterValue) -> Self {
239 match value {
240 ParameterValue::Fixed(value) => Self::Fixed(value.to_bits()),
241 ParameterValue::Param(parameter) => Self::Param(parameter.clone()),
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
248pub struct ResourceSignature {
249 pub algorithm: DecompositionAlgorithm,
250 pub ancilla_kind: RuntimeAncillaKind,
251 pub ancilla_count: usize,
252}
253
254impl ResourceSignature {
255 pub const fn no_aux(algorithm: DecompositionAlgorithm) -> Self {
257 Self {
258 algorithm,
259 ancilla_kind: RuntimeAncillaKind::None,
260 ancilla_count: 0,
261 }
262 }
263
264 pub const fn clean(algorithm: DecompositionAlgorithm, count: usize) -> Self {
266 Self {
267 algorithm,
268 ancilla_kind: RuntimeAncillaKind::CleanZero,
269 ancilla_count: count,
270 }
271 }
272
273 pub const fn dirty(algorithm: DecompositionAlgorithm, count: usize) -> Self {
275 Self {
276 algorithm,
277 ancilla_kind: RuntimeAncillaKind::Dirty,
278 ancilla_count: count,
279 }
280 }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
284pub enum RuntimeAncillaKind {
285 None,
286 CleanZero,
287 Dirty,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
292pub enum DecompositionAlgorithm {
293 PauliSmall,
294 PauliTwoClean,
295 PauliOneCleanKg24,
296 PauliManyClean,
297 PauliManyDirty,
298 PauliTwoDirty,
299 PauliOneDirty,
300 PauliOneCleanB95,
301 PauliNoAux,
302 CleanAccumulator,
303 NoAux,
304}
305
306#[derive(Debug, Clone)]
307pub struct DecompositionRule {
308 operations: Vec<TemplateOperation>,
309}
310
311impl DecompositionRule {
312 pub fn from_operations(
318 controls: &[Qubit],
319 targets: &[Qubit],
320 ancillas: &[Qubit],
321 operations: &[ValueOperation],
322 ) -> Result<Self, CompilerError> {
323 let mut template_operations = Vec::with_capacity(operations.len());
324 for operation in operations {
325 let ValueInstruction::Instruction(instruction) = &operation.instruction else {
326 return Err(CompilerError::InvariantViolation(
327 "runtime decomposition rules support only gate-like value instructions"
328 .to_string(),
329 ));
330 };
331 let qubits = operation
332 .qubits
333 .iter()
334 .map(|qubit| template_qubit(*qubit, controls, targets, ancillas))
335 .collect::<Result<SmallVec<[_; 3]>, _>>()?;
336 template_operations.push(TemplateOperation {
337 instruction: instruction.clone(),
338 qubits,
339 params: operation.params.clone(),
340 label: operation.label.clone(),
341 });
342 }
343 Ok(Self {
344 operations: template_operations,
345 })
346 }
347
348 pub fn instantiate(
350 &self,
351 controls: &[Qubit],
352 targets: &[Qubit],
353 ancillas: &[Qubit],
354 ) -> Result<Vec<ValueOperation>, CompilerError> {
355 self.operations
356 .iter()
357 .map(|operation| operation.instantiate(controls, targets, ancillas))
358 .collect()
359 }
360}
361
362#[derive(Debug, Clone)]
363struct TemplateOperation {
364 instruction: Instruction,
365 qubits: SmallVec<[TemplateQubit; 3]>,
366 params: SmallVec<[ParameterValue; 1]>,
367 label: Option<Box<str>>,
368}
369
370impl TemplateOperation {
371 fn instantiate(
372 &self,
373 controls: &[Qubit],
374 targets: &[Qubit],
375 ancillas: &[Qubit],
376 ) -> Result<ValueOperation, CompilerError> {
377 let qubits = self
378 .qubits
379 .iter()
380 .map(|qubit| qubit.instantiate(controls, targets, ancillas))
381 .collect::<Result<SmallVec<[_; 3]>, _>>()?;
382 Ok(ValueOperation {
383 instruction: ValueInstruction::from_instruction(self.instruction.clone()),
384 qubits,
385 params: self.params.clone(),
386 label: self.label.clone(),
387 })
388 }
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
392enum TemplateQubit {
393 Control(usize),
394 Target(usize),
395 Ancilla(usize),
396}
397
398impl TemplateQubit {
399 fn instantiate(
400 self,
401 controls: &[Qubit],
402 targets: &[Qubit],
403 ancillas: &[Qubit],
404 ) -> Result<Qubit, CompilerError> {
405 match self {
406 Self::Control(index) => controls.get(index).copied(),
407 Self::Target(index) => targets.get(index).copied(),
408 Self::Ancilla(index) => ancillas.get(index).copied(),
409 }
410 .ok_or_else(|| {
411 CompilerError::InvariantViolation(format!(
412 "runtime decomposition rule references missing template qubit {self:?}"
413 ))
414 })
415 }
416}
417
418fn template_qubit(
419 qubit: Qubit,
420 controls: &[Qubit],
421 targets: &[Qubit],
422 ancillas: &[Qubit],
423) -> Result<TemplateQubit, CompilerError> {
424 if let Some(index) = controls.iter().position(|control| *control == qubit) {
425 return Ok(TemplateQubit::Control(index));
426 }
427 if let Some(index) = targets.iter().position(|target| *target == qubit) {
428 return Ok(TemplateQubit::Target(index));
429 }
430 if let Some(index) = ancillas.iter().position(|ancilla| *ancilla == qubit) {
431 return Ok(TemplateQubit::Ancilla(index));
432 }
433 Err(CompilerError::InvariantViolation(format!(
434 "runtime decomposition rule output uses qubit {qubit} outside source operands and ancillas"
435 )))
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use crate::circuit::{Instruction, Parameter, Qubit, StandardGate, ValueOperation};
442 use crate::compile::transform::decompose::unitary::TwoQubitSynthesisTarget;
443 use ndarray::Array2;
444 use num_complex::Complex64;
445 use smallvec::smallvec;
446
447 fn request<'a>(
448 params: &'a [ParameterValue],
449 resource: ResourceSignature,
450 ) -> McGateRuleRequest<'a> {
451 McGateRuleRequest {
452 gate: StandardGate::RX,
453 control_count: 2,
454 target_count: 1,
455 params,
456 resource,
457 }
458 }
459
460 #[test]
461 fn cached_rule_instantiates_on_different_qubits() {
462 let params = [ParameterValue::Param(Parameter::symbol("theta"))];
463 let resource = ResourceSignature::clean(DecompositionAlgorithm::CleanAccumulator, 1);
464 let mut cache = DecompositionRuleCache::default();
465 let operations = vec![
466 ValueOperation {
467 instruction: ValueInstruction::from_instruction(Instruction::Standard(
468 StandardGate::CX,
469 )),
470 qubits: smallvec![Qubit::new(0), Qubit::new(3)],
471 params: smallvec![],
472 label: None,
473 },
474 ValueOperation {
475 instruction: ValueInstruction::from_instruction(Instruction::Standard(
476 StandardGate::RX,
477 )),
478 qubits: smallvec![Qubit::new(2)],
479 params: smallvec![params[0].clone()],
480 label: None,
481 },
482 ];
483
484 assert!(
485 cache
486 .instantiate_mc_gate(
487 request(¶ms, resource),
488 &[Qubit::new(0), Qubit::new(1)],
489 &[Qubit::new(2)],
490 &[Qubit::new(3)]
491 )
492 .unwrap()
493 .is_none()
494 );
495 cache
496 .insert_mc_gate(
497 request(¶ms, resource),
498 &[Qubit::new(0), Qubit::new(1)],
499 &[Qubit::new(2)],
500 &[Qubit::new(3)],
501 &operations,
502 )
503 .unwrap();
504
505 let instantiated = cache
506 .instantiate_mc_gate(
507 request(¶ms, resource),
508 &[Qubit::new(5), Qubit::new(6)],
509 &[Qubit::new(7)],
510 &[Qubit::new(8)],
511 )
512 .unwrap()
513 .unwrap();
514
515 assert_eq!(
516 instantiated[0].qubits.as_slice(),
517 &[Qubit::new(5), Qubit::new(8)]
518 );
519 assert_eq!(instantiated[1].qubits.as_slice(), &[Qubit::new(7)]);
520 assert_eq!(
521 cache.stats(),
522 DecompositionRuleStats {
523 hits: 1,
524 misses: 1,
525 inserts: 1,
526 }
527 );
528 }
529
530 #[test]
531 fn resource_signature_separates_otherwise_equal_rules() {
532 let params = [ParameterValue::Fixed(0.25)];
533 let mut cache = DecompositionRuleCache::default();
534 let operations = vec![ValueOperation::from_standard(
535 StandardGate::RX,
536 [Qubit::new(2)],
537 [params[0].clone()],
538 )];
539 cache
540 .insert_mc_gate(
541 request(
542 ¶ms,
543 ResourceSignature::no_aux(DecompositionAlgorithm::NoAux),
544 ),
545 &[Qubit::new(0), Qubit::new(1)],
546 &[Qubit::new(2)],
547 &[],
548 &operations,
549 )
550 .unwrap();
551
552 assert!(
553 cache
554 .instantiate_mc_gate(
555 request(
556 ¶ms,
557 ResourceSignature::clean(DecompositionAlgorithm::CleanAccumulator, 1),
558 ),
559 &[Qubit::new(0), Qubit::new(1)],
560 &[Qubit::new(2)],
561 &[Qubit::new(3)],
562 )
563 .unwrap()
564 .is_none()
565 );
566 }
567
568 #[test]
569 fn numeric_two_qubit_cache_separates_target_signatures() {
570 let matrix = Array2::<Complex64>::eye(4);
571 let cx_target = TwoQubitSynthesisTarget::from_standard_gates(
572 vec![StandardGate::U, StandardGate::H],
573 vec![StandardGate::CX],
574 true,
575 )
576 .unwrap();
577 let cz_target = TwoQubitSynthesisTarget::from_standard_gates(
578 vec![StandardGate::U, StandardGate::H],
579 vec![StandardGate::CZ],
580 true,
581 )
582 .unwrap();
583 let request = NumericUnitaryRuleRequest {
584 num_qubits: 2,
585 matrix: &matrix,
586 synthesis_key: NumericUnitarySynthesisKey::two_qubit(&cx_target),
587 };
588 let distinct_target_request = NumericUnitaryRuleRequest {
589 num_qubits: 2,
590 matrix: &matrix,
591 synthesis_key: NumericUnitarySynthesisKey::two_qubit(&cz_target),
592 };
593 let qubits = [Qubit::new(0), Qubit::new(1)];
594 let operations = vec![ValueOperation::from_standard(StandardGate::CX, qubits, [])];
595 let mut cache = DecompositionRuleCache::default();
596
597 assert!(
598 cache
599 .instantiate_numeric_unitary(request.clone(), &qubits)
600 .unwrap()
601 .is_none()
602 );
603 cache
604 .insert_numeric_unitary(request.clone(), &qubits, &operations, 0.0)
605 .unwrap();
606 assert!(
607 cache
608 .instantiate_numeric_unitary(request, &[Qubit::new(2), Qubit::new(3)])
609 .unwrap()
610 .is_some()
611 );
612 assert!(
613 cache
614 .instantiate_numeric_unitary(distinct_target_request, &qubits)
615 .unwrap()
616 .is_none()
617 );
618 assert_eq!(
619 cache.stats(),
620 DecompositionRuleStats {
621 hits: 1,
622 misses: 2,
623 inserts: 1,
624 }
625 );
626 }
627}