Skip to main content

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;