cqlib_core/circuit/ansatz/feature_map.rs
1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! # Feature Map Module
14//!
15//! This module provides feature encoding circuits for quantum machine learning.
16//! Feature maps encode classical data into quantum states, enabling quantum
17//! computers to process classical information.
18//!
19//! ## Available Encodings
20//!
21//! - [`BasisEncoding`]: Computational basis state preparation from a bitstring
22//! - [`AngleEncoding`]: Simple tensor product encoding using rotation gates
23//! - [`ZFeatureMap`]: First-order Pauli-Z feature map
24//! - [`IQPFeatureMap`]: IQP-style diagonal feature map
25//! - [`ZZFeatureMap`]: Entangling feature map with ZZ interactions
26//! - [`PauliFeatureMap`]: General-purpose feature map with arbitrary Pauli strings
27
28use super::traits::Ansatz;
29use super::two_local::EntanglementTopology;
30use crate::circuit::Instruction;
31use crate::circuit::Parameter;
32use crate::circuit::ParameterValue;
33use crate::circuit::bit::Qubit;
34use crate::circuit::circuit_impl::Circuit;
35use crate::circuit::error::CircuitError;
36use crate::circuit::gate::StandardGate;
37use crate::qis::evolution::PauliEvolution;
38use crate::qis::pauli::{Pauli, PauliString};
39
40/// Angle Encoding (also known as Tensor Product Encoding).
41///
42/// Encodes $N$ classical features into $N$ qubits by applying parameterized
43/// single-qubit rotations. A common choice is applying $R_X(x_i)$ or $R_Y(x_i)$
44/// to each qubit $i$.
45///
46/// This is the simplest feature map, mapping features independently.
47#[derive(Debug, Clone)]
48pub struct AngleEncoding {
49 num_qubits: usize,
50 rotation_gate: StandardGate,
51}
52
53impl AngleEncoding {
54 /// Creates a new AngleEncoding using the specified rotation gate.
55 /// Typically, `StandardGate::RX` or `StandardGate::RY` is used.
56 pub fn new(num_qubits: usize, rotation_gate: StandardGate) -> Self {
57 Self {
58 num_qubits,
59 rotation_gate,
60 }
61 }
62}
63
64impl Ansatz for AngleEncoding {
65 fn validate(&self) -> Result<(), CircuitError> {
66 if self.num_qubits == 0 {
67 return Err(CircuitError::InvalidOperation(
68 "AngleEncoding requires at least 1 qubit".to_string(),
69 ));
70 }
71 // Only single-parameter single-qubit rotation gates are valid
72 if !matches!(
73 self.rotation_gate,
74 StandardGate::RX | StandardGate::RY | StandardGate::RZ | StandardGate::Phase
75 ) {
76 return Err(CircuitError::InvalidOperation(format!(
77 "AngleEncoding rotation_gate must be a single-parameter single-qubit gate \
78 (RX, RY, RZ, or Phase), got {:?}",
79 self.rotation_gate
80 )));
81 }
82 Ok(())
83 }
84
85 fn build_circuit(&self, prefix: &str) -> Result<Circuit, CircuitError> {
86 self.validate()?;
87
88 let mut circuit = Circuit::new(self.num_qubits);
89
90 for q in 0..self.num_qubits {
91 let param_name = format!("{}_{}", prefix, q);
92 let param = Parameter::try_from(param_name.as_str())
93 .map_err(|_| CircuitError::InvalidParameterValue(q, f64::NAN))?;
94
95 circuit.append(
96 Instruction::Standard(self.rotation_gate),
97 vec![Qubit::new(q as u32)],
98 vec![ParameterValue::Param(param)],
99 None,
100 )?;
101 }
102
103 Ok(circuit)
104 }
105
106 fn num_parameters(&self) -> usize {
107 self.num_qubits
108 }
109
110 fn num_qubits(&self) -> usize {
111 self.num_qubits
112 }
113}
114
115/// Basis encoding into a computational basis state.
116///
117/// A boolean bitstring is mapped to a basis state by applying `X` to every qubit
118/// whose bit value is `true`. For example, `[true, false, true]` prepares `|101>`.
119/// This encoding has no trainable or symbolic parameters.
120#[derive(Debug, Clone)]
121pub struct BasisEncoding {
122 bits: Vec<bool>,
123}
124
125impl BasisEncoding {
126 /// Creates a new basis encoder from a boolean bitstring.
127 pub fn new(bits: Vec<bool>) -> Self {
128 Self { bits }
129 }
130}
131
132impl Ansatz for BasisEncoding {
133 fn validate(&self) -> Result<(), CircuitError> {
134 if self.bits.is_empty() {
135 return Err(CircuitError::InvalidOperation(
136 "BasisEncoding requires at least 1 bit".to_string(),
137 ));
138 }
139 Ok(())
140 }
141
142 fn build_circuit(&self, _prefix: &str) -> Result<Circuit, CircuitError> {
143 self.validate()?;
144
145 let mut circuit = Circuit::new(self.bits.len());
146 for (idx, bit) in self.bits.iter().enumerate() {
147 if *bit {
148 circuit.x(Qubit::new(idx as u32))?;
149 }
150 }
151 Ok(circuit)
152 }
153
154 fn num_parameters(&self) -> usize {
155 0
156 }
157
158 fn num_qubits(&self) -> usize {
159 self.bits.len()
160 }
161}
162
163/// First-order Z feature map.
164///
165/// Each repetition applies a Hadamard layer followed by independent
166/// `RZ(2 * x_i)` rotations. This is the non-entangling base case of
167/// [`ZZFeatureMap`].
168#[derive(Debug, Clone)]
169pub struct ZFeatureMap {
170 num_qubits: usize,
171 reps: usize,
172}
173
174impl ZFeatureMap {
175 /// Creates a new ZFeatureMap with `reps = 2`.
176 pub fn new(num_qubits: usize) -> Self {
177 Self {
178 num_qubits,
179 reps: 2,
180 }
181 }
182
183 /// Sets the number of repetition layers.
184 pub fn reps(mut self, reps: usize) -> Self {
185 self.reps = reps;
186 self
187 }
188}
189
190impl Ansatz for ZFeatureMap {
191 fn validate(&self) -> Result<(), CircuitError> {
192 if self.num_qubits == 0 {
193 return Err(CircuitError::InvalidOperation(
194 "ZFeatureMap requires at least 1 qubit".to_string(),
195 ));
196 }
197 Ok(())
198 }
199
200 fn build_circuit(&self, prefix: &str) -> Result<Circuit, CircuitError> {
201 self.validate()?;
202
203 let mut circuit = Circuit::new(self.num_qubits);
204 let mut x_params = Vec::with_capacity(self.num_qubits);
205 for i in 0..self.num_qubits {
206 let param_name = format!("{}_{}", prefix, i);
207 let param = Parameter::try_from(param_name.as_str())
208 .map_err(|_| CircuitError::InvalidParameterValue(i, f64::NAN))?;
209 x_params.push(param);
210 }
211
212 for _layer in 0..self.reps {
213 for q in 0..self.num_qubits {
214 circuit.h(Qubit::new(q as u32))?;
215 }
216 for (i, param) in x_params.iter().enumerate() {
217 circuit.rz(
218 Qubit::new(i as u32),
219 ParameterValue::Param(param.clone() * 2.0),
220 )?;
221 }
222 }
223
224 Ok(circuit)
225 }
226
227 fn num_parameters(&self) -> usize {
228 if self.reps == 0 {
229 return 0;
230 }
231 self.num_qubits
232 }
233
234 fn num_qubits(&self) -> usize {
235 self.num_qubits
236 }
237}
238
239/// IQP-style diagonal feature map.
240///
241/// This is an explicit named wrapper over the same diagonal ZZ feature-map
242/// structure used by [`ZZFeatureMap`]: each repetition applies a Hadamard
243/// layer, first-order Z rotations, and second-order ZZ interactions according
244/// to the selected entanglement topology.
245#[derive(Debug, Clone)]
246pub struct IQPFeatureMap {
247 inner: ZZFeatureMap,
248}
249
250impl IQPFeatureMap {
251 /// Creates a new IQPFeatureMap with `reps = 2` and full entanglement.
252 pub fn new(num_qubits: usize) -> Self {
253 Self {
254 inner: ZZFeatureMap::new(num_qubits),
255 }
256 }
257
258 /// Sets the number of repetition layers.
259 pub fn reps(mut self, reps: usize) -> Self {
260 self.inner = self.inner.reps(reps);
261 self
262 }
263
264 /// Sets the entanglement topology for two-qubit diagonal interactions.
265 pub fn entanglement(mut self, topology: EntanglementTopology) -> Self {
266 self.inner = self.inner.entanglement(topology);
267 self
268 }
269}
270
271impl Ansatz for IQPFeatureMap {
272 fn validate(&self) -> Result<(), CircuitError> {
273 self.inner.validate()
274 }
275
276 fn build_circuit(&self, prefix: &str) -> Result<Circuit, CircuitError> {
277 self.inner.build_circuit(prefix)
278 }
279
280 fn num_parameters(&self) -> usize {
281 self.inner.num_parameters()
282 }
283
284 fn num_qubits(&self) -> usize {
285 self.inner.num_qubits()
286 }
287}
288
289/// ZZ Feature Map (also known as the IQP Encoding).
290///
291/// A second-order Pauli-Z evolution circuit widely used in Quantum Kernel Methods.
292/// It encodes classical data using single-qubit Z-rotations and two-qubit ZZ-entanglement,
293/// making the feature space highly non-linear and difficult to simulate classically.
294///
295/// For data vector $x$, a single layer applies:
296/// 1. Hadamard gates on all qubits.
297/// 2. $R_Z(2 \cdot x_i)$ on each qubit $i$.
298/// 3. $e^{-i (2 \cdot (\pi - x_i)(\pi - x_j)) Z_i Z_j}$ for entangled pairs $(i,j)$.
299///
300/// Note: The specific encoding function $\phi(x)$ can vary. We use a common variation:
301/// $\phi_i(x) = x_i$ for single qubits, and $\phi_{ij}(x) = (\pi - x_i)(\pi - x_j)$ for pairs.
302#[derive(Debug, Clone)]
303pub struct ZZFeatureMap {
304 num_qubits: usize,
305 reps: usize,
306 entanglement: EntanglementTopology,
307}
308
309impl ZZFeatureMap {
310 /// Creates a new ZZFeatureMap.
311 ///
312 /// By default, it uses `reps = 2` and `EntanglementTopology::Full`.
313 pub fn new(num_qubits: usize) -> Self {
314 Self {
315 num_qubits,
316 reps: 2,
317 entanglement: EntanglementTopology::Full,
318 }
319 }
320
321 /// Sets the number of repetition layers.
322 pub fn reps(mut self, reps: usize) -> Self {
323 self.reps = reps;
324 self
325 }
326
327 /// Sets the entanglement topology.
328 pub fn entanglement(mut self, topology: EntanglementTopology) -> Self {
329 self.entanglement = topology;
330 self
331 }
332}
333
334impl Ansatz for ZZFeatureMap {
335 fn validate(&self) -> Result<(), CircuitError> {
336 if self.num_qubits == 0 {
337 return Err(CircuitError::InvalidOperation(
338 "ZZFeatureMap requires at least 1 qubit".to_string(),
339 ));
340 }
341 if let EntanglementTopology::Custom(pairs) = &self.entanglement {
342 use std::collections::HashSet;
343 let mut seen: HashSet<(usize, usize)> = HashSet::new();
344 for (i, j) in pairs {
345 if *i >= self.num_qubits || *j >= self.num_qubits {
346 return Err(CircuitError::InvalidOperation(format!(
347 "Custom entanglement topology contains out-of-bounds index: \
348 ({}, {}) for {} qubits",
349 i, j, self.num_qubits
350 )));
351 }
352 if i == j {
353 return Err(CircuitError::InvalidOperation(format!(
354 "Custom entanglement topology contains self-loop ({}, {})",
355 i, j
356 )));
357 }
358 // Undirected duplicate check
359 let edge = if i < j { (*i, *j) } else { (*j, *i) };
360 if !seen.insert(edge) {
361 return Err(CircuitError::InvalidOperation(format!(
362 "Custom entanglement topology contains duplicate edge ({}, {})",
363 i, j
364 )));
365 }
366 }
367 }
368 Ok(())
369 }
370
371 fn build_circuit(&self, prefix: &str) -> Result<Circuit, CircuitError> {
372 self.validate()?;
373
374 let mut circuit = Circuit::new(self.num_qubits);
375 let qubits = circuit.qubits();
376
377 // Prepare the base parameters [x_0, x_1, ... x_{n-1}]
378 let mut x_params = Vec::with_capacity(self.num_qubits);
379 for i in 0..self.num_qubits {
380 let param_name = format!("{}_{}", prefix, i);
381 let param = Parameter::try_from(param_name.as_str())
382 .map_err(|_| CircuitError::InvalidParameterValue(i, f64::NAN))?;
383 x_params.push(param);
384 }
385
386 // Generate the pairs based on the chosen topology
387 let pairs = self.entanglement.generate_pairs(self.num_qubits);
388
389 for _layer in 0..self.reps {
390 // 1. Initial Hadamard layer
391 for q in 0..self.num_qubits {
392 circuit.h(Qubit::new(q as u32))?;
393 }
394
395 // 2. Single qubit phase encoding: RZ(2 * x_i)
396 // U = e^{-i * x_i * Z} = RZ(2 * x_i)
397 for (i, item) in x_params.iter().enumerate().take(self.num_qubits) {
398 let angle = item.clone() * 2.0;
399 circuit.rz(Qubit::new(i as u32), ParameterValue::Param(angle))?;
400 }
401
402 // 3. Two qubit ZZ entanglement: e^{-i * 2 * (\pi - x_i)(\pi - x_j) * Z_i Z_j}
403 if !pairs.is_empty() {
404 for &(i, j) in &pairs {
405 if i < self.num_qubits && j < self.num_qubits {
406 // Let phi_{ij} = (\pi - x_i) * (\pi - x_j)
407 // Evolution: R_ZZ(\theta) = e^{-i (\theta/2) Z_i Z_j}
408 // So we want \theta/2 = 2 * phi_{ij} => \theta = 4 * phi_{ij}
409
410 let pi_param = Parameter::pi();
411 let xi = x_params[i].clone();
412 let xj = x_params[j].clone();
413
414 // construct (\pi - x_i) * (\pi - x_j)
415 let phi_ij = (pi_param.clone() - xi) * (pi_param - xj);
416 let angle = phi_ij * 4.0;
417
418 // Using pauli evolution for ZZ
419 let mut pauli_str = PauliString::new(self.num_qubits);
420 pauli_str.set_pauli(i, Pauli::Z);
421 pauli_str.set_pauli(j, Pauli::Z);
422
423 circuit.pauli_evolution(
424 &pauli_str,
425 ParameterValue::Param(angle),
426 &qubits,
427 )?;
428 }
429 }
430 }
431 }
432
433 Ok(circuit)
434 }
435
436 fn num_parameters(&self) -> usize {
437 if self.reps == 0 {
438 return 0;
439 }
440 self.num_qubits
441 }
442
443 fn num_qubits(&self) -> usize {
444 self.num_qubits
445 }
446}
447
448/// Pauli Feature Map: A general-purpose data encoding circuit using Pauli evolution.
449///
450/// The PauliFeatureMap encodes classical data into quantum states through parameterized
451/// Pauli evolution gates. It supports arbitrary Pauli strings (e.g., "Z", "ZZ", "ZZZ")
452/// and flexible entanglement topologies.
453///
454/// # Mathematical Foundation
455///
456/// For a k-local Pauli template P and feature vector x, the encoding applies:
457///
458/// | Locality | Evolution | Angle θ (passed to `pauli_evolution`) |
459/// |----------|-----------|----------------------------------------|
460/// | k = 1 | $e^{-i x_i P}$ | $2 x_i$ |
461/// | k ≥ 2 | $e^{-i 2 \prod_{j \in S}(\pi - x_j) P}$ | $4 \prod_{j \in S}(\pi - x_j)$ |
462///
463/// where `pauli_evolution(θ)` implements $e^{-i \frac{\theta}{2} P}$.
464///
465/// The non-linear k-local mapping $\prod_{j \in S}(\pi - x_j)$ creates a rich
466/// feature space through the kernel trick, making the kernel function hard to
467/// evaluate classically for large circuits.
468///
469/// # Architecture
470///
471/// The circuit structure for each repetition layer:
472///
473/// 1. **Hadamard Layer**: Apply $H$ gates to all qubits to create superposition.
474/// 2. **k-local Pauli Evolution**: For each Pauli template P and each k-tuple
475/// of qubit indices $(q_0, \ldots, q_{k-1})$ from the entanglement topology,
476/// apply $e^{-i \phi(x_{q_0}, \ldots, x_{q_{k-1}}) P}$.
477///
478/// # Example
479///
480/// ```rust
481/// use cqlib_core::circuit::ansatz::{Ansatz, PauliFeatureMap, EntanglementTopology};
482/// use cqlib_core::qis::pauli::PauliString;
483///
484/// // 3-qubit feature map with Z (1-local) + ZZ (2-local) + ZZZ (3-local)
485/// let feature_map = PauliFeatureMap::new(3)
486/// .reps(1)
487/// .paulis(vec![
488/// ("Z".parse::<PauliString>().unwrap(), "Z".to_string()),
489/// ("ZZ".parse::<PauliString>().unwrap(), "ZZ".to_string()),
490/// ("ZZZ".parse::<PauliString>().unwrap(), "ZZZ".to_string()),
491/// ])
492/// .entanglement(EntanglementTopology::Full);
493///
494/// let circuit = feature_map.build_circuit("x").unwrap();
495/// assert_eq!(feature_map.num_parameters(), 3);
496/// ```
497#[derive(Debug, Clone)]
498pub struct PauliFeatureMap {
499 /// Number of qubits (features) in the feature map.
500 num_qubits: usize,
501 /// Number of repetition layers for the encoding.
502 reps: usize,
503 /// List of Pauli strings to use for evolution, with their labels.
504 paulis: Vec<(PauliString, String)>,
505 /// Entanglement topology for 2-local interactions.
506 entanglement: EntanglementTopology,
507 /// Prefix for parameter names (default: "x").
508 parameter_prefix: String,
509}
510
511impl PauliFeatureMap {
512 /// Creates a new PauliFeatureMap with default configuration.
513 ///
514 /// # Default Configuration
515 ///
516 /// - `reps`: 2 layers
517 /// - `paulis`: ["Z", "ZZ"] (single-qubit Z and two-qubit ZZ interactions)
518 /// - `entanglement`: [`Full`](EntanglementTopology::Full) all-to-all connectivity
519 /// - `parameter_prefix`: "x"
520 ///
521 /// # Arguments
522 ///
523 /// * `num_qubits` - The number of qubits (features) in the feature map.
524 ///
525 /// # Example
526 ///
527 /// ```rust
528 /// use cqlib_core::circuit::ansatz::{Ansatz, PauliFeatureMap};
529 ///
530 /// let fm = PauliFeatureMap::new(4);
531 /// assert_eq!(fm.num_qubits(), 4);
532 /// ```
533 pub fn new(num_qubits: usize) -> Self {
534 Self {
535 num_qubits,
536 reps: 2,
537 paulis: vec![
538 ("Z".parse::<PauliString>().unwrap(), "Z".to_string()),
539 ("ZZ".parse::<PauliString>().unwrap(), "ZZ".to_string()),
540 ],
541 entanglement: EntanglementTopology::Full,
542 parameter_prefix: "x".to_string(),
543 }
544 }
545
546 /// Sets the number of repetition layers.
547 ///
548 /// Each repetition applies the full encoding sequence (Hadamard + Pauli evolutions).
549 /// More repetitions increase the expressiveness of the feature map but also
550 /// increase circuit depth.
551 ///
552 /// # Arguments
553 ///
554 /// * `reps` - The number of repetition layers.
555 pub fn reps(mut self, reps: usize) -> Self {
556 self.reps = reps;
557 self
558 }
559
560 /// Sets the Pauli strings to use for evolution.
561 ///
562 /// Each Pauli string specifies a type of interaction:
563 /// - Single-character strings (e.g., "Z", "X", "Y") create 1-local interactions.
564 /// - Multi-character strings (e.g., "ZZ", "XY") create multi-qubit interactions.
565 ///
566 /// # Arguments
567 ///
568 /// * `paulis` - A vector of `(PauliString, label)` tuples.
569 pub fn paulis(mut self, paulis: Vec<(PauliString, String)>) -> Self {
570 self.paulis = paulis;
571 self
572 }
573
574 /// Sets the entanglement topology for 2-local interactions.
575 ///
576 /// The topology determines which qubit pairs are connected by 2-local
577 /// Pauli evolution gates.
578 ///
579 /// # Arguments
580 ///
581 /// * `topology` - The entanglement topology.
582 pub fn entanglement(mut self, topology: EntanglementTopology) -> Self {
583 self.entanglement = topology;
584 self
585 }
586
587 /// Sets the parameter name prefix.
588 ///
589 /// Parameter names are generated as `{prefix}_{index}` (e.g., "x_0", "x_1").
590 ///
591 /// # Arguments
592 ///
593 /// * `prefix` - The parameter name prefix.
594 pub fn parameter_prefix(mut self, prefix: impl Into<String>) -> Self {
595 self.parameter_prefix = prefix.into();
596 self
597 }
598}
599
600impl Ansatz for PauliFeatureMap {
601 fn validate(&self) -> Result<(), CircuitError> {
602 // Check that num_qubits > 0
603 if self.num_qubits == 0 {
604 return Err(CircuitError::InvalidOperation(
605 "PauliFeatureMap requires at least 1 qubit".to_string(),
606 ));
607 }
608
609 // Check that pauli strings have length <= num_qubits
610 for (pauli_str, _) in &self.paulis {
611 if pauli_str.num_qubits > self.num_qubits {
612 return Err(CircuitError::InvalidOperation(format!(
613 "Pauli string '{}' has length {} which exceeds num_qubits {}",
614 pauli_str, pauli_str.num_qubits, self.num_qubits
615 )));
616 }
617 }
618
619 // If Custom topology, validate all indices and reject ambiguous edges.
620 if let EntanglementTopology::Custom(pairs) = &self.entanglement {
621 use std::collections::HashSet;
622 let mut seen: HashSet<(usize, usize)> = HashSet::new();
623 for (i, j) in pairs {
624 if *i >= self.num_qubits || *j >= self.num_qubits {
625 return Err(CircuitError::InvalidOperation(format!(
626 "Custom entanglement topology contains out-of-bounds index: ({}, {}) for {} qubits",
627 i, j, self.num_qubits
628 )));
629 }
630 if i == j {
631 return Err(CircuitError::InvalidOperation(format!(
632 "Custom entanglement topology contains self-loop ({}, {})",
633 i, j
634 )));
635 }
636 let edge = if i < j { (*i, *j) } else { (*j, *i) };
637 if !seen.insert(edge) {
638 return Err(CircuitError::InvalidOperation(format!(
639 "Custom entanglement topology contains duplicate edge ({}, {})",
640 i, j
641 )));
642 }
643 }
644 }
645
646 Ok(())
647 }
648
649 fn build_circuit(&self, prefix: &str) -> Result<Circuit, CircuitError> {
650 // Validate configuration first
651 self.validate()?;
652
653 let mut circuit = Circuit::new(self.num_qubits);
654 let qubits = circuit.qubits();
655
656 // Use provided prefix if non-empty, otherwise use self.parameter_prefix
657 let effective_prefix = if prefix.is_empty() {
658 &self.parameter_prefix
659 } else {
660 prefix
661 };
662
663 // Prepare the base parameters [x_0, x_1, ..., x_{n-1}]
664 let mut x_params = Vec::with_capacity(self.num_qubits);
665 for i in 0..self.num_qubits {
666 let param_name = format!("{}_{}", effective_prefix, i);
667 let param = Parameter::try_from(param_name.as_str())
668 .map_err(|_| CircuitError::InvalidParameterValue(i, f64::NAN))?;
669 x_params.push(param);
670 }
671
672 // Apply the encoding for each repetition layer
673 for _layer in 0..self.reps {
674 // Step 1: Initial Hadamard layer to create superposition
675 for q in 0..self.num_qubits {
676 circuit.h(Qubit::new(q as u32))?;
677 }
678
679 // Step 2: Apply Pauli evolution for each Pauli string
680 for (pauli_str, _) in &self.paulis {
681 // Determine the support (non-identity positions) of the template Pauli string.
682 let support = pauli_str.support();
683 let k = support.len();
684
685 // An all-identity Pauli string contributes only a global phase; skip it.
686 if k == 0 {
687 continue;
688 }
689
690 // Extract the Pauli operator type at each support position.
691 let template_ops: Vec<Pauli> = support
692 .iter()
693 .map(|&idx| pauli_str.get_pauli(idx))
694 .collect();
695
696 // Generate all k-tuples of circuit qubit indices for this k-local interaction.
697 // For k=1: each single qubit independently.
698 // For k≥2: k-tuples from the chosen entanglement topology.
699 let tuples = self.entanglement.generate_k_tuples(k, self.num_qubits);
700
701 for tuple in &tuples {
702 // Build a full-length PauliString that places each template operator
703 // at the corresponding circuit qubit position in the tuple.
704 let mut full_pauli = PauliString::new(self.num_qubits);
705 for (pos, &qubit_idx) in tuple.iter().enumerate() {
706 full_pauli.set_pauli(qubit_idx, template_ops[pos]);
707 }
708
709 // Compute the encoding angle θ passed to `pauli_evolution`,
710 // which implements e^{-i θ/2 P}:
711 //
712 // k=1: θ = 2·x_i → e^{-i x_i P}
713 // k≥2: θ = 4·∏(π−x_j) → e^{-i 2·∏(π−x_j) P}
714 let angle = if k == 1 {
715 x_params[tuple[0]].clone() * 2.0
716 } else {
717 let pi = Parameter::pi();
718 let mut product = pi.clone() - x_params[tuple[0]].clone();
719 for &qi in &tuple[1..] {
720 product = product * (pi.clone() - x_params[qi].clone());
721 }
722 product * 4.0
723 };
724
725 circuit.pauli_evolution(&full_pauli, ParameterValue::Param(angle), &qubits)?;
726 }
727 }
728 }
729
730 Ok(circuit)
731 }
732
733 fn num_parameters(&self) -> usize {
734 // The number of parameters equals the number of features (qubits)
735 // Each qubit has one input feature parameter x_i
736 if self.reps == 0 {
737 return 0;
738 }
739 self.num_qubits
740 }
741
742 fn num_qubits(&self) -> usize {
743 self.num_qubits
744 }
745}
746
747#[cfg(test)]
748#[path = "feature_map_test.rs"]
749mod feature_map_test;