Skip to main content

quantrs2_core/qml/
nlp.rs

1//! Quantum Machine Learning for Natural Language Processing
2//!
3//! This module provides specialized quantum machine learning layers and algorithms
4//! optimized for natural language processing tasks such as text classification,
5//! sentiment analysis, and language modeling.
6
7use super::{Parameter, QMLLayer};
8use crate::{
9    error::{QuantRS2Error, QuantRS2Result},
10    gate::{multi::*, single::*, GateOp},
11    parametric::{ParametricRotationX, ParametricRotationY, ParametricRotationZ},
12    qubit::QubitId,
13};
14use scirs2_core::ndarray::Array1;
15use scirs2_core::Complex64;
16use std::collections::HashMap;
17use std::f64::consts::PI;
18
19/// Determine the number of qubits a gate sequence acts on.
20///
21/// Returns `max(min_qubits, highest_targeted_qubit_index + 1)` so the simulated
22/// state vector is always large enough for every gate while never shrinking
23/// below the model's declared register size.
24fn circuit_num_qubits(gates: &[Box<dyn GateOp>], min_qubits: usize) -> usize {
25    let max_index = gates
26        .iter()
27        .flat_map(|gate| gate.qubits())
28        .map(|q| q.0 as usize)
29        .max();
30    match max_index {
31        Some(idx) => min_qubits.max(idx + 1),
32        None => min_qubits.max(1),
33    }
34}
35
36/// Number of qubits required to address `count` distinct outcomes,
37/// i.e. `ceil(log2(count))` (at least 1).
38fn readout_bits(count: usize) -> usize {
39    if count <= 1 {
40        1
41    } else {
42        (usize::BITS - (count - 1).leading_zeros()) as usize
43    }
44}
45
46/// Text embedding strategies for quantum NLP
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum TextEmbeddingStrategy {
49    /// Word-level embeddings: each word is encoded separately
50    WordLevel,
51    /// Character-level embeddings: each character is encoded
52    CharLevel,
53    /// N-gram embeddings: overlapping n-grams are encoded
54    NGram(usize),
55    /// Token embeddings with positional encoding
56    TokenPositional,
57    /// Hierarchical embeddings: words -> sentences -> documents
58    Hierarchical,
59}
60
61/// Configuration for quantum NLP models
62#[derive(Debug, Clone)]
63pub struct QNLPConfig {
64    /// Number of qubits for text representation
65    pub text_qubits: usize,
66    /// Number of qubits for feature extraction
67    pub feature_qubits: usize,
68    /// Maximum sequence length
69    pub max_sequence_length: usize,
70    /// Vocabulary size
71    pub vocab_size: usize,
72    /// Embedding dimension
73    pub embedding_dim: usize,
74    /// Text embedding strategy
75    pub embedding_strategy: TextEmbeddingStrategy,
76    /// Number of attention heads (for quantum attention)
77    pub num_attention_heads: usize,
78    /// Hidden dimension for feedforward layers
79    pub hidden_dim: usize,
80}
81
82impl Default for QNLPConfig {
83    fn default() -> Self {
84        Self {
85            text_qubits: 8,
86            feature_qubits: 4,
87            max_sequence_length: 32,
88            vocab_size: 1000,
89            embedding_dim: 64,
90            embedding_strategy: TextEmbeddingStrategy::WordLevel,
91            num_attention_heads: 4,
92            hidden_dim: 128,
93        }
94    }
95}
96
97/// Quantum word embedding layer
98pub struct QuantumWordEmbedding {
99    /// Configuration
100    config: QNLPConfig,
101    /// Embedding parameters for each word in vocabulary
102    embeddings: Vec<Vec<Parameter>>,
103    /// Flattened view of all embedding parameters (row-major: word_id * num_qubits + qubit)
104    /// This cache is the single source of truth exposed via the QMLLayer trait.
105    /// It is kept in sync with `embeddings` via `rebuild_flat_cache` and `sync_from_flat`.
106    flat_params: Vec<Parameter>,
107    /// Number of qubits
108    num_qubits: usize,
109}
110
111impl QuantumWordEmbedding {
112    /// Create a new quantum word embedding layer
113    pub fn new(config: QNLPConfig) -> Self {
114        let num_qubits = config.text_qubits;
115        let mut embeddings = Vec::new();
116        let mut flat_params: Vec<Parameter> = Vec::new();
117
118        // Initialize embeddings for each word in vocabulary
119        for word_id in 0..config.vocab_size {
120            let mut word_embedding = Vec::new();
121            for qubit in 0..num_qubits {
122                // Initialize with deterministic pseudo-random values
123                let value = ((word_id * qubit.max(1)) as f64 * 0.1).sin() * 0.5;
124                let param = Parameter {
125                    name: format!("embed_{word_id}_{qubit}"),
126                    value,
127                    bounds: None,
128                };
129                flat_params.push(param.clone());
130                word_embedding.push(param);
131            }
132            embeddings.push(word_embedding);
133        }
134
135        Self {
136            config,
137            embeddings,
138            flat_params,
139            num_qubits,
140        }
141    }
142
143    /// Rebuild the flat parameter cache from the nested embeddings.
144    fn rebuild_flat_cache(&mut self) {
145        self.flat_params.clear();
146        for word_emb in &self.embeddings {
147            self.flat_params.extend(word_emb.iter().cloned());
148        }
149    }
150
151    /// Sync the nested embeddings from the flat parameter cache after an
152    /// external mutation through `parameters_mut()`.
153    fn sync_from_flat(&mut self) {
154        let nq = self.num_qubits;
155        for (word_id, word_emb) in self.embeddings.iter_mut().enumerate() {
156            for (qubit, param) in word_emb.iter_mut().enumerate() {
157                let flat_idx = word_id * nq + qubit;
158                if let Some(flat_param) = self.flat_params.get(flat_idx) {
159                    param.value = flat_param.value;
160                }
161            }
162        }
163    }
164
165    /// Encode a sequence of word IDs into quantum gates
166    pub fn encode_sequence(&self, word_ids: &[usize]) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
167        let mut gates: Vec<Box<dyn GateOp>> = Vec::new();
168        let nq = self.num_qubits;
169
170        for (position, &word_id) in word_ids.iter().enumerate() {
171            if word_id >= self.config.vocab_size {
172                return Err(QuantRS2Error::InvalidInput(format!(
173                    "Word ID {} exceeds vocabulary size {}",
174                    word_id, self.config.vocab_size
175                )));
176            }
177
178            if position >= self.config.max_sequence_length {
179                break; // Truncate sequence if too long
180            }
181
182            // Read embedding values from the flat_params cache (canonical store)
183            let flat_base = word_id * nq;
184            for qubit_idx in 0..nq {
185                let flat_idx = flat_base + qubit_idx;
186                let value = self
187                    .flat_params
188                    .get(flat_idx)
189                    .map(|p| p.value)
190                    .unwrap_or(0.0);
191
192                let qubit = QubitId(qubit_idx as u32);
193
194                // Use rotation gates to encode the embedding values
195                gates.push(Box::new(ParametricRotationY {
196                    target: qubit,
197                    theta: crate::parametric::Parameter::Constant(value * PI),
198                }));
199
200                // Add positional encoding (sinusoidal, scaled to small contribution)
201                let positional_angle =
202                    (position as f64) / (self.config.max_sequence_length as f64) * PI;
203                gates.push(Box::new(ParametricRotationZ {
204                    target: qubit,
205                    theta: crate::parametric::Parameter::Constant(positional_angle * 0.1),
206                }));
207            }
208        }
209
210        Ok(gates)
211    }
212}
213
214impl QMLLayer for QuantumWordEmbedding {
215    fn num_qubits(&self) -> usize {
216        self.num_qubits
217    }
218
219    fn parameters(&self) -> &[Parameter] {
220        // Return the pre-built flat cache.  The cache is row-major over
221        // (word_id, qubit) and built on construction; it is also updated
222        // whenever set_parameters() is called via parameters_mut().
223        &self.flat_params
224    }
225
226    fn parameters_mut(&mut self) -> &mut [Parameter] {
227        // Callers mutate the flat cache.  The nested `embeddings` field is
228        // a convenience copy that is kept in sync by sync_from_flat(), which
229        // is called by the default set_parameters() implementation via this
230        // method.  If callers mutate flat_params directly (e.g. in a training
231        // loop) they should call sync_from_flat() before using encode_sequence.
232        &mut self.flat_params
233    }
234
235    fn gates(&self) -> Vec<Box<dyn GateOp>> {
236        // Return empty - this layer provides encoding method
237        Vec::new()
238    }
239
240    fn compute_gradients(
241        &self,
242        _state: &Array1<Complex64>,
243        _loss_gradient: &Array1<Complex64>,
244    ) -> QuantRS2Result<Vec<f64>> {
245        // Placeholder for gradient computation
246        let total_params = self.config.vocab_size * self.num_qubits;
247        Ok(vec![0.0; total_params])
248    }
249
250    fn name(&self) -> &'static str {
251        "QuantumWordEmbedding"
252    }
253}
254
255/// Quantum attention mechanism for NLP
256pub struct QuantumAttention {
257    /// Number of qubits
258    num_qubits: usize,
259    /// Number of attention heads
260    num_heads: usize,
261    /// Query parameters
262    query_params: Vec<Parameter>,
263    /// Key parameters
264    key_params: Vec<Parameter>,
265    /// Value parameters
266    value_params: Vec<Parameter>,
267    /// Output projection parameters
268    output_params: Vec<Parameter>,
269    /// Flattened view: [query... | key... | value... | output...]
270    /// Used by the QMLLayer trait (parameters / parameters_mut).
271    flat_params: Vec<Parameter>,
272}
273
274impl QuantumAttention {
275    /// Create a new quantum attention layer
276    pub fn new(num_qubits: usize, num_heads: usize) -> Self {
277        let params_per_head = num_qubits / num_heads.max(1);
278
279        let mut query_params = Vec::new();
280        let mut key_params = Vec::new();
281        let mut value_params = Vec::new();
282        let mut output_params = Vec::new();
283
284        // Initialize parameters for each head
285        for head in 0..num_heads {
286            for i in 0..params_per_head {
287                // Query parameters
288                query_params.push(Parameter {
289                    name: format!("query_{head}_{i}"),
290                    value: ((head + i) as f64 * 0.1).sin() * 0.5,
291                    bounds: None,
292                });
293
294                // Key parameters
295                key_params.push(Parameter {
296                    name: format!("key_{head}_{i}"),
297                    value: ((head + i + 1) as f64 * 0.1).cos() * 0.5,
298                    bounds: None,
299                });
300
301                // Value parameters
302                value_params.push(Parameter {
303                    name: format!("value_{head}_{i}"),
304                    value: ((head + i + 2) as f64 * 0.1).sin() * 0.5,
305                    bounds: None,
306                });
307
308                // Output parameters
309                output_params.push(Parameter {
310                    name: format!("output_{head}_{i}"),
311                    value: ((head + i + 3) as f64 * 0.1).cos() * 0.5,
312                    bounds: None,
313                });
314            }
315        }
316
317        // Build the flat cache: query | key | value | output
318        let mut flat_params: Vec<Parameter> = Vec::new();
319        flat_params.extend(query_params.iter().cloned());
320        flat_params.extend(key_params.iter().cloned());
321        flat_params.extend(value_params.iter().cloned());
322        flat_params.extend(output_params.iter().cloned());
323
324        Self {
325            num_qubits,
326            num_heads,
327            query_params,
328            key_params,
329            value_params,
330            output_params,
331            flat_params,
332        }
333    }
334
335    /// Rebuild the flat cache from the four per-group parameter vectors.
336    pub fn rebuild_flat_cache(&mut self) {
337        self.flat_params.clear();
338        self.flat_params.extend(self.query_params.iter().cloned());
339        self.flat_params.extend(self.key_params.iter().cloned());
340        self.flat_params.extend(self.value_params.iter().cloned());
341        self.flat_params.extend(self.output_params.iter().cloned());
342    }
343
344    /// Sync the four per-group parameter vectors from the flat cache.
345    pub fn sync_from_flat(&mut self) {
346        let qlen = self.query_params.len();
347        let klen = self.key_params.len();
348        let vlen = self.value_params.len();
349
350        for (i, p) in self.query_params.iter_mut().enumerate() {
351            if let Some(fp) = self.flat_params.get(i) {
352                p.value = fp.value;
353            }
354        }
355        for (i, p) in self.key_params.iter_mut().enumerate() {
356            if let Some(fp) = self.flat_params.get(qlen + i) {
357                p.value = fp.value;
358            }
359        }
360        for (i, p) in self.value_params.iter_mut().enumerate() {
361            if let Some(fp) = self.flat_params.get(qlen + klen + i) {
362                p.value = fp.value;
363            }
364        }
365        for (i, p) in self.output_params.iter_mut().enumerate() {
366            if let Some(fp) = self.flat_params.get(qlen + klen + vlen + i) {
367                p.value = fp.value;
368            }
369        }
370    }
371
372    /// Generate attention gates for a sequence
373    pub fn attention_gates(&self) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
374        let mut gates: Vec<Box<dyn GateOp>> = Vec::new();
375        let params_per_head = self.num_qubits / self.num_heads;
376
377        // For each attention head
378        for head in 0..self.num_heads {
379            let head_offset = head * params_per_head;
380
381            // Apply query transformations
382            for i in 0..params_per_head {
383                let qubit = QubitId((head_offset + i) as u32);
384                let param_idx = head * params_per_head + i;
385
386                gates.push(Box::new(ParametricRotationY {
387                    target: qubit,
388                    theta: crate::parametric::Parameter::Constant(
389                        self.query_params[param_idx].value,
390                    ),
391                }));
392            }
393
394            // Apply key transformations
395            for i in 0..params_per_head {
396                let qubit = QubitId((head_offset + i) as u32);
397                let param_idx = head * params_per_head + i;
398
399                gates.push(Box::new(ParametricRotationZ {
400                    target: qubit,
401                    theta: crate::parametric::Parameter::Constant(self.key_params[param_idx].value),
402                }));
403            }
404
405            // Add entanglement within head (for attention computation).
406            // `saturating_sub` guards the `params_per_head == 0` case (more heads
407            // than qubits), which would otherwise underflow.
408            for i in 0..params_per_head.saturating_sub(1) {
409                let control = QubitId((head_offset + i) as u32);
410                let target = QubitId((head_offset + i + 1) as u32);
411                gates.push(Box::new(CNOT { control, target }));
412            }
413
414            // Apply value transformations
415            for i in 0..params_per_head {
416                let qubit = QubitId((head_offset + i) as u32);
417                let param_idx = head * params_per_head + i;
418
419                gates.push(Box::new(ParametricRotationX {
420                    target: qubit,
421                    theta: crate::parametric::Parameter::Constant(
422                        self.value_params[param_idx].value,
423                    ),
424                }));
425            }
426        }
427
428        // Add inter-head entanglement (for multi-head attention). Skipped when
429        // a head spans zero qubits (more heads than qubits), which would make
430        // control == target.
431        if params_per_head > 0 {
432            for head in 0..self.num_heads.saturating_sub(1) {
433                let control = QubitId((head * params_per_head) as u32);
434                let target = QubitId(((head + 1) * params_per_head) as u32);
435                if control.0 != target.0 {
436                    gates.push(Box::new(CNOT { control, target }));
437                }
438            }
439        }
440
441        // Apply output projection
442        for i in 0..self.output_params.len() {
443            let qubit = QubitId(i as u32);
444            gates.push(Box::new(ParametricRotationY {
445                target: qubit,
446                theta: crate::parametric::Parameter::Constant(self.output_params[i].value),
447            }));
448        }
449
450        Ok(gates)
451    }
452}
453
454impl QMLLayer for QuantumAttention {
455    fn num_qubits(&self) -> usize {
456        self.num_qubits
457    }
458
459    fn parameters(&self) -> &[Parameter] {
460        // Return the pre-built flat cache [query | key | value | output].
461        // The cache is constructed in `new()` and can be refreshed with
462        // `rebuild_flat_cache()` if the per-group Vecs are mutated directly.
463        &self.flat_params
464    }
465
466    fn parameters_mut(&mut self) -> &mut [Parameter] {
467        // Callers may mutate via this slice; call sync_from_flat() afterwards
468        // to propagate changes back to the per-group parameter Vecs used in
469        // attention_gates().
470        &mut self.flat_params
471    }
472
473    fn gates(&self) -> Vec<Box<dyn GateOp>> {
474        self.attention_gates().unwrap_or_default()
475    }
476
477    fn compute_gradients(
478        &self,
479        _state: &Array1<Complex64>,
480        _loss_gradient: &Array1<Complex64>,
481    ) -> QuantRS2Result<Vec<f64>> {
482        let total_params = self.query_params.len()
483            + self.key_params.len()
484            + self.value_params.len()
485            + self.output_params.len();
486        Ok(vec![0.0; total_params])
487    }
488
489    fn name(&self) -> &'static str {
490        "QuantumAttention"
491    }
492}
493
494/// Quantum text classifier for sentiment analysis and text classification
495pub struct QuantumTextClassifier {
496    /// Configuration
497    config: QNLPConfig,
498    /// Word embedding layer
499    embedding: QuantumWordEmbedding,
500    /// Attention layers
501    attention_layers: Vec<QuantumAttention>,
502    /// Classification parameters
503    classifier_params: Vec<Parameter>,
504    /// Number of output classes
505    num_classes: usize,
506}
507
508impl QuantumTextClassifier {
509    /// Create a new quantum text classifier
510    pub fn new(config: QNLPConfig, num_classes: usize) -> Self {
511        let embedding = QuantumWordEmbedding::new(config.clone());
512
513        // Create multiple attention layers for deeper models
514        let mut attention_layers = Vec::new();
515        for _layer_idx in 0..2 {
516            // 2 attention layers
517            attention_layers.push(QuantumAttention::new(
518                config.text_qubits,
519                config.num_attention_heads,
520            ));
521        }
522
523        // Create classification parameters
524        let mut classifier_params = Vec::new();
525        for class in 0..num_classes {
526            for qubit in 0..config.feature_qubits {
527                classifier_params.push(Parameter {
528                    name: format!("classifier_{class}_{qubit}"),
529                    value: ((class + qubit) as f64 * 0.2).sin() * 0.3,
530                    bounds: None,
531                });
532            }
533        }
534
535        Self {
536            config,
537            embedding,
538            attention_layers,
539            classifier_params,
540            num_classes,
541        }
542    }
543
544    /// Classify a text sequence by running the full quantum forward pass.
545    ///
546    /// The classification circuit is built and simulated exactly; the resulting
547    /// `2^n` Born-rule distribution is reduced to `num_classes` class scores by
548    /// marginalising the basis states over the low-order `ceil(log2(num_classes))`
549    /// readout qubits, then renormalised. This is a genuine measurement
550    /// distribution, not a heuristic.
551    pub fn classify(&self, word_ids: &[usize]) -> QuantRS2Result<Vec<f64>> {
552        let gates = self.build_circuit(word_ids)?;
553        let num_qubits = circuit_num_qubits(&gates, self.config.text_qubits);
554        let state = crate::qml::simulator::simulate(num_qubits, &gates)?;
555        let amplitudes = crate::qml::simulator::probabilities(&state);
556
557        // Number of readout qubits needed to address all classes.
558        let class_bits = readout_bits(self.num_classes);
559        let class_mask = (1usize << class_bits) - 1;
560
561        let mut probs = vec![0.0; self.num_classes];
562        for (basis_index, prob) in amplitudes.iter().enumerate() {
563            let class = basis_index & class_mask;
564            if class < self.num_classes {
565                probs[class] += prob;
566            }
567        }
568
569        // Renormalise (basis states whose readout exceeds num_classes are dropped).
570        let sum: f64 = probs.iter().sum();
571        if sum > 0.0 {
572            for prob in &mut probs {
573                *prob /= sum;
574            }
575        } else {
576            // Degenerate circuit (e.g. all probability mass on dropped states):
577            // fall back to a uniform distribution rather than zeros.
578            let uniform = 1.0 / self.num_classes as f64;
579            probs.fill(uniform);
580        }
581
582        Ok(probs)
583    }
584
585    /// Generate the full circuit for text classification
586    pub fn build_circuit(&self, word_ids: &[usize]) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
587        let mut gates = Vec::new();
588
589        // 1. Word embedding
590        gates.extend(self.embedding.encode_sequence(word_ids)?);
591
592        // 2. Attention layers
593        for attention in &self.attention_layers {
594            gates.extend(attention.attention_gates()?);
595        }
596
597        // 3. Feature extraction and pooling (using measurement-like operations)
598        // This would include global pooling operations
599        for qubit in 0..self.config.text_qubits {
600            gates.push(Box::new(Hadamard {
601                target: QubitId(qubit as u32),
602            }));
603        }
604
605        // 4. Classification layer
606        for (_class, chunk) in self
607            .classifier_params
608            .chunks(self.config.feature_qubits)
609            .enumerate()
610        {
611            for (i, param) in chunk.iter().enumerate() {
612                let qubit = QubitId(i as u32);
613                gates.push(Box::new(ParametricRotationY {
614                    target: qubit,
615                    theta: crate::parametric::Parameter::Constant(param.value),
616                }));
617            }
618        }
619
620        Ok(gates)
621    }
622
623    /// Train the classifier using a dataset
624    pub fn train(
625        &mut self,
626        training_data: &[(Vec<usize>, usize)],
627        learning_rate: f64,
628        epochs: usize,
629    ) -> QuantRS2Result<Vec<f64>> {
630        let mut losses = Vec::new();
631
632        for epoch in 0..epochs {
633            let mut epoch_loss = 0.0;
634
635            for (word_ids, true_label) in training_data {
636                // Forward pass
637                let predictions = self.classify(word_ids)?;
638
639                // Compute loss (cross-entropy)
640                let loss = -predictions[*true_label].ln();
641                epoch_loss += loss;
642
643                // Backward pass (simplified gradient computation)
644                // In practice, this would use automatic differentiation
645                self.update_parameters(predictions, *true_label, learning_rate)?;
646            }
647
648            epoch_loss /= training_data.len() as f64;
649            losses.push(epoch_loss);
650
651            if epoch % 10 == 0 {
652                println!("Epoch {epoch}: Loss = {epoch_loss:.4}");
653            }
654        }
655
656        Ok(losses)
657    }
658
659    /// Update parameters based on gradients (simplified)
660    fn update_parameters(
661        &mut self,
662        predictions: Vec<f64>,
663        true_label: usize,
664        learning_rate: f64,
665    ) -> QuantRS2Result<()> {
666        // Simplified parameter update
667        // In practice, would compute proper gradients using parameter shift rule
668
669        for (i, param) in self.classifier_params.iter_mut().enumerate() {
670            // All parameters are learnable in this simplified implementation
671            {
672                let class_idx = i / self.config.feature_qubits;
673                let error = if class_idx == true_label {
674                    predictions[class_idx] - 1.0
675                } else {
676                    predictions[class_idx]
677                };
678
679                // Simple gradient descent update
680                param.value -= learning_rate * error * 0.1;
681            }
682        }
683
684        Ok(())
685    }
686}
687
688/// Quantum language model for text generation
689pub struct QuantumLanguageModel {
690    /// Configuration
691    config: QNLPConfig,
692    /// Embedding layer
693    embedding: QuantumWordEmbedding,
694    /// Transformer layers
695    transformer_layers: Vec<QuantumAttention>,
696    /// Output parameters
697    output_params: Vec<Parameter>,
698}
699
700impl QuantumLanguageModel {
701    /// Create a new quantum language model
702    pub fn new(config: QNLPConfig) -> Self {
703        let embedding = QuantumWordEmbedding::new(config.clone());
704
705        // Create transformer layers
706        let mut transformer_layers = Vec::new();
707        for _layer in 0..3 {
708            // 3 transformer layers
709            transformer_layers.push(QuantumAttention::new(
710                config.text_qubits,
711                config.num_attention_heads,
712            ));
713        }
714
715        // Create output parameters for next token prediction
716        let mut output_params = Vec::new();
717        for token in 0..config.vocab_size {
718            output_params.push(Parameter {
719                name: format!("output_{token}"),
720                value: (token as f64 * 0.01).sin() * 0.1,
721                bounds: None,
722            });
723        }
724
725        Self {
726            config,
727            embedding,
728            transformer_layers,
729            output_params,
730        }
731    }
732
733    /// Generate next-token probabilities by exactly simulating the context
734    /// circuit and reading out a `vocab_size`-way Born-rule distribution.
735    ///
736    /// The circuit's `2^n` measurement distribution is marginalised over the
737    /// low-order `ceil(log2(vocab_size))` readout qubits to obtain per-token
738    /// probabilities, then renormalised. This is a real quantum measurement
739    /// distribution, not a heuristic placeholder.
740    pub fn predict_next_token(&self, context: &[usize]) -> QuantRS2Result<Vec<f64>> {
741        let gates = self.build_circuit(context)?;
742        let num_qubits = circuit_num_qubits(&gates, self.config.text_qubits);
743        let state = crate::qml::simulator::simulate(num_qubits, &gates)?;
744        let amplitudes = crate::qml::simulator::probabilities(&state);
745
746        let vocab = self.config.vocab_size;
747        let token_bits = readout_bits(vocab);
748        let token_mask = (1usize << token_bits) - 1;
749
750        let mut probs = vec![0.0; vocab];
751        for (basis_index, prob) in amplitudes.iter().enumerate() {
752            let token = basis_index & token_mask;
753            if token < vocab {
754                probs[token] += prob;
755            }
756        }
757
758        let sum: f64 = probs.iter().sum();
759        if sum > 0.0 {
760            for prob in &mut probs {
761                *prob /= sum;
762            }
763        } else {
764            let uniform = 1.0 / vocab as f64;
765            probs.fill(uniform);
766        }
767
768        Ok(probs)
769    }
770
771    /// Generate text given a starting context
772    pub fn generate_text(
773        &self,
774        start_context: &[usize],
775        max_length: usize,
776        temperature: f64,
777    ) -> QuantRS2Result<Vec<usize>> {
778        let mut generated = start_context.to_vec();
779
780        for _step in 0..max_length {
781            // Get context (last N tokens)
782            let context_start = if generated.len() > self.config.max_sequence_length {
783                generated.len() - self.config.max_sequence_length
784            } else {
785                0
786            };
787            let context = &generated[context_start..];
788
789            // Predict next token
790            let mut probs = self.predict_next_token(context)?;
791
792            // Apply temperature scaling
793            if temperature != 1.0 {
794                for prob in &mut probs {
795                    *prob = (*prob).powf(1.0 / temperature);
796                }
797                let sum: f64 = probs.iter().sum();
798                for prob in &mut probs {
799                    *prob /= sum;
800                }
801            }
802
803            // Sample next token (using simple deterministic selection for now)
804            let next_token = probs
805                .iter()
806                .enumerate()
807                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
808                .map(|(i, _)| i)
809                .unwrap_or(0);
810
811            generated.push(next_token);
812        }
813
814        Ok(generated)
815    }
816
817    /// Build the full language model circuit
818    fn build_circuit(&self, context: &[usize]) -> QuantRS2Result<Vec<Box<dyn GateOp>>> {
819        let mut gates = Vec::new();
820
821        // 1. Embedding
822        gates.extend(self.embedding.encode_sequence(context)?);
823
824        // 2. Transformer layers
825        for transformer in &self.transformer_layers {
826            gates.extend(transformer.attention_gates()?);
827        }
828
829        // 3. Output projection
830        for (i, param) in self.output_params.iter().enumerate() {
831            let qubit = QubitId((i % self.config.text_qubits) as u32);
832            gates.push(Box::new(ParametricRotationZ {
833                target: qubit,
834                theta: crate::parametric::Parameter::Constant(param.value),
835            }));
836        }
837
838        Ok(gates)
839    }
840}
841
842#[cfg(test)]
843mod tests {
844    use super::*;
845
846    #[test]
847    fn test_quantum_word_embedding() {
848        let config = QNLPConfig {
849            vocab_size: 100,
850            text_qubits: 4,
851            ..Default::default()
852        };
853
854        let embedding = QuantumWordEmbedding::new(config);
855        assert_eq!(embedding.num_qubits(), 4);
856
857        // Test encoding a simple sequence
858        let word_ids = vec![1, 5, 10];
859        let gates = embedding
860            .encode_sequence(&word_ids)
861            .expect("Failed to encode sequence");
862        assert!(!gates.is_empty());
863    }
864
865    #[test]
866    fn test_quantum_attention() {
867        let attention = QuantumAttention::new(8, 2);
868        assert_eq!(attention.num_qubits(), 8);
869        assert_eq!(attention.num_heads, 2);
870
871        let gates = attention
872            .attention_gates()
873            .expect("Failed to get attention gates");
874        assert!(!gates.is_empty());
875    }
876
877    #[test]
878    fn test_quantum_text_classifier() {
879        let config = QNLPConfig {
880            vocab_size: 50,
881            text_qubits: 4,
882            feature_qubits: 2,
883            ..Default::default()
884        };
885
886        let classifier = QuantumTextClassifier::new(config, 3);
887
888        // Test classification
889        let word_ids = vec![1, 2, 3];
890        let probs = classifier
891            .classify(&word_ids)
892            .expect("Failed to classify text");
893        assert_eq!(probs.len(), 3);
894
895        // Check probabilities sum to 1
896        let sum: f64 = probs.iter().sum();
897        assert!((sum - 1.0).abs() < 1e-10);
898    }
899
900    #[test]
901    fn test_quantum_language_model() {
902        let config = QNLPConfig {
903            vocab_size: 20,
904            text_qubits: 4,
905            max_sequence_length: 8,
906            ..Default::default()
907        };
908
909        let lm = QuantumLanguageModel::new(config);
910
911        // Test next token prediction
912        let context = vec![1, 2, 3];
913        let probs = lm
914            .predict_next_token(&context)
915            .expect("Failed to predict next token");
916        assert_eq!(probs.len(), 20);
917
918        // Test text generation
919        let generated = lm
920            .generate_text(&context, 5, 1.0)
921            .expect("Failed to generate text");
922        assert_eq!(generated.len(), 8); // 3 context + 5 generated
923    }
924
925    #[test]
926    fn test_text_classifier_training() {
927        let config = QNLPConfig {
928            vocab_size: 10,
929            text_qubits: 3,
930            feature_qubits: 2,
931            ..Default::default()
932        };
933
934        let mut classifier = QuantumTextClassifier::new(config, 2);
935
936        // Create dummy training data
937        let training_data = vec![
938            (vec![1, 2], 0), // Class 0
939            (vec![3, 4], 1), // Class 1
940            (vec![1, 3], 0), // Class 0
941            (vec![2, 4], 1), // Class 1
942        ];
943
944        let losses = classifier
945            .train(&training_data, 0.01, 5)
946            .expect("Failed to train classifier");
947        assert_eq!(losses.len(), 5);
948    }
949
950    #[test]
951    fn test_classify_returns_real_distribution() {
952        let config = QNLPConfig {
953            vocab_size: 50,
954            text_qubits: 3,
955            feature_qubits: 2,
956            ..Default::default()
957        };
958        let classifier = QuantumTextClassifier::new(config, 3);
959
960        let probs = classifier.classify(&[1, 5, 9]).expect("classify");
961        assert_eq!(probs.len(), 3);
962        let sum: f64 = probs.iter().sum();
963        assert!(
964            (sum - 1.0).abs() < 1e-9,
965            "class probabilities must sum to 1"
966        );
967        assert!(probs.iter().all(|&p| (-1e-12..=1.0 + 1e-9).contains(&p)));
968        // A real measurement distribution from a non-trivial circuit is not
969        // exactly uniform (the old fabrication was uniform + sin noise, but the
970        // real Born distribution from H + parameterized rotations is non-uniform).
971        let uniform = 1.0 / 3.0;
972        let max_dev = probs
973            .iter()
974            .map(|&p| (p - uniform).abs())
975            .fold(0.0, f64::max);
976        assert!(
977            max_dev > 1e-9,
978            "real circuit should not give exactly uniform output"
979        );
980    }
981
982    #[test]
983    fn test_predict_next_token_returns_real_distribution() {
984        let config = QNLPConfig {
985            vocab_size: 8,
986            text_qubits: 3,
987            ..Default::default()
988        };
989        let lm = QuantumLanguageModel::new(config);
990
991        let probs = lm.predict_next_token(&[1, 2]).expect("predict");
992        assert_eq!(probs.len(), 8);
993        let sum: f64 = probs.iter().sum();
994        assert!(
995            (sum - 1.0).abs() < 1e-9,
996            "token probabilities must sum to 1"
997        );
998        assert!(probs.iter().all(|&p| (-1e-12..=1.0 + 1e-9).contains(&p)));
999    }
1000}
1001
1002/// Advanced quantum NLP utilities and algorithms
1003pub mod advanced {
1004    use super::*;
1005
1006    /// Quantum text preprocessing utilities
1007    pub struct QuantumTextPreprocessor {
1008        /// Vocabulary mapping
1009        vocab: HashMap<String, usize>,
1010        /// Reverse vocabulary mapping
1011        reverse_vocab: HashMap<usize, String>,
1012        /// Special tokens
1013        special_tokens: HashMap<String, usize>,
1014    }
1015
1016    impl QuantumTextPreprocessor {
1017        /// Create a new preprocessor
1018        pub fn new() -> Self {
1019            let mut special_tokens = HashMap::new();
1020            special_tokens.insert("<PAD>".to_string(), 0);
1021            special_tokens.insert("<UNK>".to_string(), 1);
1022            special_tokens.insert("<START>".to_string(), 2);
1023            special_tokens.insert("<END>".to_string(), 3);
1024
1025            Self {
1026                vocab: HashMap::new(),
1027                reverse_vocab: HashMap::new(),
1028                special_tokens,
1029            }
1030        }
1031
1032        /// Build vocabulary from text corpus
1033        pub fn build_vocab(&mut self, texts: &[String], max_vocab_size: usize) {
1034            let mut word_counts: HashMap<String, usize> = HashMap::new();
1035
1036            // Count word frequencies
1037            for text in texts {
1038                for word in text.split_whitespace() {
1039                    *word_counts.entry(word.to_lowercase()).or_insert(0) += 1;
1040                }
1041            }
1042
1043            // Sort by frequency and take top words
1044            let mut word_freq: Vec<_> = word_counts.into_iter().collect();
1045            word_freq.sort_by_key(|b| std::cmp::Reverse(b.1));
1046
1047            // Add special tokens first
1048            for (token, id) in &self.special_tokens {
1049                self.vocab.insert(token.clone(), *id);
1050                self.reverse_vocab.insert(*id, token.clone());
1051            }
1052
1053            // Add most frequent words
1054            let mut vocab_id = self.special_tokens.len();
1055            for (word, _count) in word_freq
1056                .into_iter()
1057                .take(max_vocab_size - self.special_tokens.len())
1058            {
1059                self.vocab.insert(word.clone(), vocab_id);
1060                self.reverse_vocab.insert(vocab_id, word);
1061                vocab_id += 1;
1062            }
1063        }
1064
1065        /// Tokenize text to word IDs
1066        pub fn tokenize(&self, text: &str) -> Vec<usize> {
1067            let mut tokens = vec![self.special_tokens["<START>"]];
1068
1069            for word in text.split_whitespace() {
1070                let word = word.to_lowercase();
1071                let token_id = self
1072                    .vocab
1073                    .get(&word)
1074                    .copied()
1075                    .unwrap_or_else(|| self.special_tokens["<UNK>"]);
1076                tokens.push(token_id);
1077            }
1078
1079            tokens.push(self.special_tokens["<END>"]);
1080            tokens
1081        }
1082
1083        /// Convert token IDs back to text
1084        pub fn detokenize(&self, token_ids: &[usize]) -> String {
1085            token_ids
1086                .iter()
1087                .filter_map(|&id| self.reverse_vocab.get(&id))
1088                .filter(|&word| !["<PAD>", "<START>", "<END>"].contains(&word.as_str()))
1089                .cloned()
1090                .collect::<Vec<_>>()
1091                .join(" ")
1092        }
1093
1094        /// Get vocabulary size
1095        pub fn vocab_size(&self) -> usize {
1096            self.vocab.len()
1097        }
1098    }
1099
1100    /// Quantum semantic similarity computation
1101    pub struct QuantumSemanticSimilarity {
1102        /// Embedding dimension
1103        embedding_dim: usize,
1104        /// Number of qubits
1105        num_qubits: usize,
1106        /// Similarity computation parameters
1107        similarity_params: Vec<Parameter>,
1108    }
1109
1110    impl QuantumSemanticSimilarity {
1111        /// Create a new quantum semantic similarity computer
1112        pub fn new(embedding_dim: usize, num_qubits: usize) -> Self {
1113            let mut similarity_params = Vec::new();
1114
1115            // Parameters for similarity computation
1116            for i in 0..num_qubits * 2 {
1117                // For two text inputs
1118                similarity_params.push(Parameter {
1119                    name: format!("sim_{i}"),
1120                    value: (i as f64 * 0.1).sin() * 0.5,
1121                    bounds: None,
1122                });
1123            }
1124
1125            Self {
1126                embedding_dim,
1127                num_qubits,
1128                similarity_params,
1129            }
1130        }
1131
1132        /// Compute semantic similarity between two texts
1133        pub fn compute_similarity(
1134            &self,
1135            text1_tokens: &[usize],
1136            text2_tokens: &[usize],
1137        ) -> QuantRS2Result<f64> {
1138            // Create embeddings for both texts
1139            let config = QNLPConfig {
1140                text_qubits: self.num_qubits,
1141                vocab_size: 1000, // Default
1142                ..Default::default()
1143            };
1144
1145            let embedding1 = QuantumWordEmbedding::new(config.clone());
1146            let embedding2 = QuantumWordEmbedding::new(config);
1147
1148            // Generate quantum circuits for both texts
1149            let gates1 = embedding1.encode_sequence(text1_tokens)?;
1150            let gates2 = embedding2.encode_sequence(text2_tokens)?;
1151
1152            // Compute similarity using quantum interference
1153            // This is a simplified version - full implementation would measure overlap
1154            let similarity = self.quantum_text_overlap(gates1, gates2)?;
1155
1156            Ok(similarity)
1157        }
1158
1159        /// Compute the quantum overlap (state fidelity) between two text
1160        /// representations.
1161        ///
1162        /// Both gate sequences are simulated from `|0…0⟩` to obtain the encoded
1163        /// states `|ψ₁⟩` and `|ψ₂⟩`; the returned similarity is the fidelity
1164        /// `|⟨ψ₁|ψ₂⟩|² ∈ [0, 1]`. This is a genuine inner-product computation,
1165        /// not a constant.
1166        fn quantum_text_overlap(
1167            &self,
1168            gates1: Vec<Box<dyn GateOp>>,
1169            gates2: Vec<Box<dyn GateOp>>,
1170        ) -> QuantRS2Result<f64> {
1171            let num_qubits = circuit_num_qubits(&gates1, self.num_qubits)
1172                .max(circuit_num_qubits(&gates2, self.num_qubits));
1173            let psi1 = crate::qml::simulator::simulate(num_qubits, &gates1)?;
1174            let psi2 = crate::qml::simulator::simulate(num_qubits, &gates2)?;
1175
1176            let overlap: Complex64 = psi1
1177                .iter()
1178                .zip(psi2.iter())
1179                .map(|(a, b)| a.conj() * b)
1180                .sum();
1181            Ok(overlap.norm_sqr())
1182        }
1183    }
1184
1185    /// Quantum text summarization model
1186    pub struct QuantumTextSummarizer {
1187        /// Configuration
1188        config: QNLPConfig,
1189        /// Encoder for input text
1190        encoder: QuantumWordEmbedding,
1191        /// Attention mechanism for importance scoring
1192        attention: QuantumAttention,
1193        /// Summary generation parameters
1194        summary_params: Vec<Parameter>,
1195    }
1196
1197    impl QuantumTextSummarizer {
1198        /// Create a new quantum text summarizer
1199        pub fn new(config: QNLPConfig) -> Self {
1200            let encoder = QuantumWordEmbedding::new(config.clone());
1201            let attention = QuantumAttention::new(config.text_qubits, config.num_attention_heads);
1202
1203            let mut summary_params = Vec::new();
1204            for i in 0..config.text_qubits {
1205                summary_params.push(Parameter {
1206                    name: format!("summary_{i}"),
1207                    value: (i as f64 * 0.15).sin() * 0.4,
1208                    bounds: None,
1209                });
1210            }
1211
1212            Self {
1213                config,
1214                encoder,
1215                attention,
1216                summary_params,
1217            }
1218        }
1219
1220        /// Generate extractive summary from input text
1221        pub fn extractive_summarize(
1222            &self,
1223            text_tokens: &[usize],
1224            summary_length: usize,
1225        ) -> QuantRS2Result<Vec<usize>> {
1226            // Encode input text
1227            let _encoding_gates = self.encoder.encode_sequence(text_tokens)?;
1228
1229            // Apply attention to find important tokens
1230            let _attention_gates = self.attention.attention_gates()?;
1231
1232            // Score tokens for importance (simplified)
1233            let mut token_scores = Vec::new();
1234            for (i, &token) in text_tokens.iter().enumerate() {
1235                // Simple scoring based on token frequency and position
1236                let position_weight = (i as f64 / text_tokens.len() as f64).mul_add(-0.5, 1.0);
1237                let token_weight = (token as f64 * 0.1).sin().abs();
1238                let score = position_weight * token_weight;
1239                token_scores.push((i, token, score));
1240            }
1241
1242            // Sort by score and select top tokens
1243            token_scores.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
1244
1245            let mut summary_tokens = Vec::new();
1246            for (_, token, _) in token_scores.into_iter().take(summary_length) {
1247                summary_tokens.push(token);
1248            }
1249
1250            Ok(summary_tokens)
1251        }
1252
1253        /// Generate abstractive summary (placeholder)
1254        pub fn abstractive_summarize(
1255            &self,
1256            _text_tokens: &[usize],
1257            _summary_length: usize,
1258        ) -> QuantRS2Result<Vec<usize>> {
1259            // Placeholder for abstractive summarization
1260            // Would use sequence-to-sequence quantum model
1261            Ok(vec![1, 2, 3]) // Dummy summary
1262        }
1263    }
1264
1265    /// Quantum named entity recognition
1266    pub struct QuantumNamedEntityRecognition {
1267        /// Configuration
1268        config: QNLPConfig,
1269        /// Token encoder
1270        encoder: QuantumWordEmbedding,
1271        /// Entity type classifiers
1272        entity_classifiers: HashMap<String, Vec<Parameter>>,
1273        /// Supported entity types
1274        entity_types: Vec<String>,
1275    }
1276
1277    impl QuantumNamedEntityRecognition {
1278        /// Create a new quantum NER model
1279        pub fn new(config: QNLPConfig) -> Self {
1280            let encoder = QuantumWordEmbedding::new(config.clone());
1281            let entity_types = vec![
1282                "PERSON".to_string(),
1283                "ORGANIZATION".to_string(),
1284                "LOCATION".to_string(),
1285                "DATE".to_string(),
1286                "MONEY".to_string(),
1287            ];
1288
1289            let mut entity_classifiers = HashMap::new();
1290            for entity_type in &entity_types {
1291                let mut classifier_params = Vec::new();
1292                for i in 0..config.text_qubits {
1293                    classifier_params.push(Parameter {
1294                        name: format!("{entity_type}_{i}"),
1295                        value: (i as f64).mul_add(0.1, entity_type.len() as f64).sin() * 0.3,
1296                        bounds: None,
1297                    });
1298                }
1299                entity_classifiers.insert(entity_type.clone(), classifier_params);
1300            }
1301
1302            Self {
1303                config,
1304                encoder,
1305                entity_classifiers,
1306                entity_types,
1307            }
1308        }
1309
1310        /// Recognize named entities in text
1311        pub fn recognize_entities(
1312            &self,
1313            text_tokens: &[usize],
1314        ) -> QuantRS2Result<Vec<(usize, usize, String)>> {
1315            let mut entities = Vec::new();
1316
1317            // Simple sliding window approach
1318            for start in 0..text_tokens.len() {
1319                for end in start + 1..=text_tokens.len().min(start + 5) {
1320                    // Max entity length 5
1321                    let entity_tokens = &text_tokens[start..end];
1322
1323                    // Classify this span
1324                    if let Some(entity_type) = self.classify_span(entity_tokens)? {
1325                        entities.push((start, end, entity_type));
1326                    }
1327                }
1328            }
1329
1330            // Remove overlapping entities (keep longer ones)
1331            entities.sort_by_key(|b| std::cmp::Reverse(b.1 - b.0));
1332            let mut final_entities = Vec::new();
1333            let mut used_positions = vec![false; text_tokens.len()];
1334
1335            for (start, end, entity_type) in entities {
1336                if used_positions[start..end].iter().all(|&used| !used) {
1337                    for pos in start..end {
1338                        used_positions[pos] = true;
1339                    }
1340                    final_entities.push((start, end, entity_type));
1341                }
1342            }
1343
1344            final_entities.sort_by_key(|&(start, _, _)| start);
1345            Ok(final_entities)
1346        }
1347
1348        /// Classify a span of tokens as an entity type
1349        fn classify_span(&self, tokens: &[usize]) -> QuantRS2Result<Option<String>> {
1350            // Encode the span
1351            let _encoding_gates = self.encoder.encode_sequence(tokens)?;
1352
1353            let mut best_score = 0.0;
1354            let mut best_type = None;
1355
1356            // Score each entity type
1357            for entity_type in &self.entity_types {
1358                let score = self.compute_entity_score(tokens, entity_type)?;
1359                if score > best_score && score > 0.5 {
1360                    // Threshold
1361                    best_score = score;
1362                    best_type = Some(entity_type.clone());
1363                }
1364            }
1365
1366            Ok(best_type)
1367        }
1368
1369        /// Compute score for a specific entity type
1370        fn compute_entity_score(&self, tokens: &[usize], entity_type: &str) -> QuantRS2Result<f64> {
1371            // Simple scoring based on token patterns
1372            let mut score = 0.0;
1373
1374            for &token in tokens {
1375                // Simple heuristics based on token ID patterns
1376                match entity_type {
1377                    "PERSON" => {
1378                        if token % 7 == 1 {
1379                            // Arbitrary pattern for person names
1380                            score += 0.3;
1381                        }
1382                    }
1383                    "LOCATION" => {
1384                        if token % 5 == 2 {
1385                            // Arbitrary pattern for locations
1386                            score += 0.3;
1387                        }
1388                    }
1389                    "ORGANIZATION" => {
1390                        if token % 11 == 3 {
1391                            // Arbitrary pattern for organizations
1392                            score += 0.3;
1393                        }
1394                    }
1395                    "DATE" => {
1396                        if token % 13 == 4 {
1397                            // Arbitrary pattern for dates
1398                            score += 0.3;
1399                        }
1400                    }
1401                    "MONEY" => {
1402                        if token % 17 == 5 {
1403                            // Arbitrary pattern for money
1404                            score += 0.3;
1405                        }
1406                    }
1407                    _ => {}
1408                }
1409            }
1410
1411            score /= tokens.len() as f64; // Normalize by span length
1412            Ok(score)
1413        }
1414    }
1415
1416    #[cfg(test)]
1417    mod advanced_tests {
1418        use super::*;
1419
1420        #[test]
1421        fn test_text_overlap_is_real_fidelity_not_constant() {
1422            let sim = QuantumSemanticSimilarity::new(4, 3);
1423
1424            // Identical token sequences -> identical states -> fidelity == 1.
1425            let same = sim
1426                .compute_similarity(&[1, 2, 3], &[1, 2, 3])
1427                .expect("same similarity");
1428            assert!(
1429                (same - 1.0).abs() < 1e-9,
1430                "identical texts must have fidelity 1.0, got {same} (old fabrication returned 0.7)"
1431            );
1432
1433            // Different sequences -> generally < 1 and != the old 0.7 constant.
1434            let diff = sim
1435                .compute_similarity(&[1, 2, 3], &[3, 2, 1])
1436                .expect("diff similarity");
1437            assert!((0.0..=1.0 + 1e-9).contains(&diff));
1438            assert!(
1439                (diff - same).abs() > 1e-9 || (diff - 0.7).abs() > 1e-9,
1440                "similarity must be a real computed fidelity, not a constant"
1441            );
1442        }
1443    }
1444}
1445
1446// Re-export advanced utilities
1447pub use advanced::*;