quantrs2_core/qml/
quantum_contrastive.rs

1//! Quantum Contrastive Learning
2//!
3//! This module implements contrastive learning methods for quantum machine learning,
4//! enabling self-supervised representation learning without labeled data.
5//!
6//! # Theoretical Background
7//!
8//! Quantum contrastive learning extends classical contrastive methods (SimCLR, MoCo)
9//! to the quantum domain. The key idea is to learn quantum representations that
10//! maximize agreement between differently augmented views of the same quantum state
11//! while minimizing agreement with other states.
12//!
13//! # Key Components
14//!
15//! - **Quantum Data Augmentation**: Noise channels, rotations, and decoherence
16//! - **Quantum Encoder**: Parameterized quantum circuit for representation
17//! - **Contrastive Loss**: Quantum fidelity-based NT-Xent loss
18//! - **Momentum Encoder**: Slow-moving encoder for stable learning
19//!
20//! # References
21//!
22//! - "Quantum Contrastive Learning for Noisy Datasets" (2023)
23//! - "Self-Supervised Quantum Machine Learning" (2024)
24//! - "Contrastive Learning with Quantum Neural Networks" (2024)
25
26use crate::{
27    error::{QuantRS2Error, QuantRS2Result},
28    gate::GateOp,
29    qubit::QubitId,
30};
31use scirs2_core::ndarray::{Array1, Array2, Axis};
32use scirs2_core::random::prelude::*;
33use scirs2_core::Complex64;
34use std::f64::consts::PI;
35
36/// Configuration for quantum contrastive learning
37#[derive(Debug, Clone)]
38pub struct QuantumContrastiveConfig {
39    /// Number of qubits
40    pub num_qubits: usize,
41    /// Encoder depth
42    pub encoder_depth: usize,
43    /// Temperature parameter for contrastive loss
44    pub temperature: f64,
45    /// Momentum coefficient for momentum encoder
46    pub momentum: f64,
47    /// Batch size
48    pub batch_size: usize,
49    /// Number of augmentation views
50    pub num_views: usize,
51}
52
53impl Default for QuantumContrastiveConfig {
54    fn default() -> Self {
55        Self {
56            num_qubits: 4,
57            encoder_depth: 4,
58            temperature: 0.5,
59            momentum: 0.999,
60            batch_size: 32,
61            num_views: 2,
62        }
63    }
64}
65
66/// Quantum data augmentation strategies
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum QuantumAugmentation {
69    /// Random unitary rotations
70    RandomRotation,
71    /// Depolarizing noise
72    DepolarizingNoise,
73    /// Amplitude damping
74    AmplitudeDamping,
75    /// Phase damping
76    PhaseDamping,
77    /// Random Pauli gates
78    RandomPauli,
79    /// Circuit cutting and rejoining
80    CircuitCutting,
81}
82
83/// Quantum augmenter for creating multiple views of quantum states
84#[derive(Debug, Clone)]
85pub struct QuantumAugmenter {
86    /// Number of qubits
87    num_qubits: usize,
88    /// Augmentation strategies
89    strategies: Vec<QuantumAugmentation>,
90    /// Noise strength
91    noise_strength: f64,
92}
93
94impl QuantumAugmenter {
95    /// Create new quantum augmenter
96    pub fn new(
97        num_qubits: usize,
98        strategies: Vec<QuantumAugmentation>,
99        noise_strength: f64,
100    ) -> Self {
101        Self {
102            num_qubits,
103            strategies,
104            noise_strength,
105        }
106    }
107
108    /// Augment a quantum state
109    pub fn augment(
110        &self,
111        state: &Array1<Complex64>,
112        strategy: QuantumAugmentation,
113    ) -> QuantRS2Result<Array1<Complex64>> {
114        match strategy {
115            QuantumAugmentation::RandomRotation => self.random_rotation(state),
116            QuantumAugmentation::DepolarizingNoise => self.depolarizing_noise(state),
117            QuantumAugmentation::AmplitudeDamping => self.amplitude_damping(state),
118            QuantumAugmentation::PhaseDamping => self.phase_damping(state),
119            QuantumAugmentation::RandomPauli => self.random_pauli(state),
120            QuantumAugmentation::CircuitCutting => self.circuit_cutting(state),
121        }
122    }
123
124    /// Apply random unitary rotation
125    fn random_rotation(&self, state: &Array1<Complex64>) -> QuantRS2Result<Array1<Complex64>> {
126        let mut rng = thread_rng();
127        let mut new_state = state.clone();
128
129        // Apply random single-qubit rotations
130        for q in 0..self.num_qubits {
131            let angle = rng.gen_range(-PI..PI) * self.noise_strength;
132            let axis = rng.gen_range(0..3); // X, Y, or Z
133
134            new_state = match axis {
135                0 => self.apply_rx(&new_state, q, angle)?,
136                1 => self.apply_ry(&new_state, q, angle)?,
137                _ => self.apply_rz(&new_state, q, angle)?,
138            };
139        }
140
141        Ok(new_state)
142    }
143
144    /// Apply depolarizing noise
145    fn depolarizing_noise(&self, state: &Array1<Complex64>) -> QuantRS2Result<Array1<Complex64>> {
146        let mut rng = thread_rng();
147        let p = self.noise_strength;
148        let dim = state.len();
149        let mut new_state = state.clone();
150
151        // With probability p, replace with maximally mixed state
152        if rng.gen::<f64>() < p {
153            let uniform_val = Complex64::new(1.0 / (dim as f64).sqrt(), 0.0);
154            new_state = Array1::from_elem(dim, uniform_val);
155        }
156
157        Ok(new_state)
158    }
159
160    /// Apply amplitude damping (simulates energy relaxation)
161    fn amplitude_damping(&self, state: &Array1<Complex64>) -> QuantRS2Result<Array1<Complex64>> {
162        let gamma = self.noise_strength;
163        let mut new_state = state.clone();
164
165        for q in 0..self.num_qubits {
166            new_state = self.apply_amplitude_damping_qubit(&new_state, q, gamma)?;
167        }
168
169        Ok(new_state)
170    }
171
172    /// Apply amplitude damping to single qubit
173    fn apply_amplitude_damping_qubit(
174        &self,
175        state: &Array1<Complex64>,
176        qubit: usize,
177        gamma: f64,
178    ) -> QuantRS2Result<Array1<Complex64>> {
179        let dim = state.len();
180        let mut new_state = state.clone();
181
182        let k0_coeff = 1.0;
183        let k1_coeff = gamma.sqrt();
184
185        for i in 0..dim {
186            let bit = (i >> qubit) & 1;
187            if bit == 1 {
188                let j = i ^ (1 << qubit);
189                // |1⟩ → √γ |0⟩
190                new_state[j] = new_state[j] + state[i] * k1_coeff;
191                new_state[i] = state[i] * ((1.0 - gamma).sqrt());
192            }
193        }
194
195        Ok(new_state)
196    }
197
198    /// Apply phase damping (simulates dephasing)
199    fn phase_damping(&self, state: &Array1<Complex64>) -> QuantRS2Result<Array1<Complex64>> {
200        let lambda = self.noise_strength;
201        let mut new_state = state.clone();
202
203        for q in 0..self.num_qubits {
204            new_state = self.apply_phase_damping_qubit(&new_state, q, lambda)?;
205        }
206
207        Ok(new_state)
208    }
209
210    /// Apply phase damping to single qubit
211    fn apply_phase_damping_qubit(
212        &self,
213        state: &Array1<Complex64>,
214        qubit: usize,
215        lambda: f64,
216    ) -> QuantRS2Result<Array1<Complex64>> {
217        let dim = state.len();
218        let mut new_state = state.clone();
219
220        let damp_factor = (1.0 - lambda).sqrt();
221
222        for i in 0..dim {
223            let bit = (i >> qubit) & 1;
224            if bit == 1 {
225                new_state[i] = state[i] * damp_factor;
226            }
227        }
228
229        Ok(new_state)
230    }
231
232    /// Apply random Pauli gates
233    fn random_pauli(&self, state: &Array1<Complex64>) -> QuantRS2Result<Array1<Complex64>> {
234        let mut rng = thread_rng();
235        let mut new_state = state.clone();
236
237        for q in 0..self.num_qubits {
238            if rng.gen::<f64>() < self.noise_strength {
239                let pauli = rng.gen_range(0..4); // I, X, Y, Z
240                new_state = match pauli {
241                    1 => self.apply_pauli_x(&new_state, q)?,
242                    2 => self.apply_pauli_y(&new_state, q)?,
243                    3 => self.apply_pauli_z(&new_state, q)?,
244                    _ => new_state,
245                };
246            }
247        }
248
249        Ok(new_state)
250    }
251
252    /// Circuit cutting augmentation
253    fn circuit_cutting(&self, state: &Array1<Complex64>) -> QuantRS2Result<Array1<Complex64>> {
254        // For now, just apply a combination of other augmentations
255        let mut new_state = state.clone();
256        new_state = self.random_rotation(&new_state)?;
257        new_state = self.phase_damping(&new_state)?;
258        Ok(new_state)
259    }
260
261    /// Helper: Apply Pauli-X
262    fn apply_pauli_x(
263        &self,
264        state: &Array1<Complex64>,
265        qubit: usize,
266    ) -> QuantRS2Result<Array1<Complex64>> {
267        let dim = state.len();
268        let mut new_state = state.clone();
269
270        for i in 0..dim {
271            let j = i ^ (1 << qubit);
272            if i < j {
273                let temp = new_state[i];
274                new_state[i] = new_state[j];
275                new_state[j] = temp;
276            }
277        }
278
279        Ok(new_state)
280    }
281
282    /// Helper: Apply Pauli-Y
283    fn apply_pauli_y(
284        &self,
285        state: &Array1<Complex64>,
286        qubit: usize,
287    ) -> QuantRS2Result<Array1<Complex64>> {
288        let dim = state.len();
289        let mut new_state = state.clone();
290
291        for i in 0..dim {
292            let bit = (i >> qubit) & 1;
293            let j = i ^ (1 << qubit);
294            if i < j {
295                let factor = if bit == 0 {
296                    Complex64::new(0.0, 1.0)
297                } else {
298                    Complex64::new(0.0, -1.0)
299                };
300                let temp = new_state[i];
301                new_state[i] = new_state[j] * factor;
302                new_state[j] = temp * (-factor);
303            }
304        }
305
306        Ok(new_state)
307    }
308
309    /// Helper: Apply Pauli-Z
310    fn apply_pauli_z(
311        &self,
312        state: &Array1<Complex64>,
313        qubit: usize,
314    ) -> QuantRS2Result<Array1<Complex64>> {
315        let dim = state.len();
316        let mut new_state = state.clone();
317
318        for i in 0..dim {
319            let bit = (i >> qubit) & 1;
320            if bit == 1 {
321                new_state[i] = -new_state[i];
322            }
323        }
324
325        Ok(new_state)
326    }
327
328    /// Helper rotation functions
329    fn apply_rx(
330        &self,
331        state: &Array1<Complex64>,
332        qubit: usize,
333        angle: f64,
334    ) -> QuantRS2Result<Array1<Complex64>> {
335        let dim = state.len();
336        let mut new_state = Array1::zeros(dim);
337
338        let cos_half = Complex64::new((angle / 2.0).cos(), 0.0);
339        let sin_half = Complex64::new(0.0, -(angle / 2.0).sin());
340
341        for i in 0..dim {
342            let j = i ^ (1 << qubit);
343            new_state[i] = state[i] * cos_half + state[j] * sin_half;
344        }
345
346        Ok(new_state)
347    }
348
349    fn apply_ry(
350        &self,
351        state: &Array1<Complex64>,
352        qubit: usize,
353        angle: f64,
354    ) -> QuantRS2Result<Array1<Complex64>> {
355        let dim = state.len();
356        let mut new_state = Array1::zeros(dim);
357
358        let cos_half = (angle / 2.0).cos();
359        let sin_half = (angle / 2.0).sin();
360
361        for i in 0..dim {
362            let bit = (i >> qubit) & 1;
363            let j = i ^ (1 << qubit);
364
365            if bit == 0 {
366                new_state[i] = state[i] * cos_half - state[j] * sin_half;
367            } else {
368                new_state[i] = state[i] * cos_half + state[j] * sin_half;
369            }
370        }
371
372        Ok(new_state)
373    }
374
375    fn apply_rz(
376        &self,
377        state: &Array1<Complex64>,
378        qubit: usize,
379        angle: f64,
380    ) -> QuantRS2Result<Array1<Complex64>> {
381        let dim = state.len();
382        let mut new_state = state.clone();
383
384        let phase = Complex64::new((angle / 2.0).cos(), -(angle / 2.0).sin());
385
386        for i in 0..dim {
387            let bit = (i >> qubit) & 1;
388            new_state[i] = if bit == 1 {
389                new_state[i] * phase
390            } else {
391                new_state[i] * phase.conj()
392            };
393        }
394
395        Ok(new_state)
396    }
397}
398
399/// Quantum encoder network
400#[derive(Debug, Clone)]
401pub struct QuantumEncoder {
402    /// Number of qubits
403    num_qubits: usize,
404    /// Circuit depth
405    depth: usize,
406    /// Parameters
407    params: Array2<f64>,
408}
409
410impl QuantumEncoder {
411    /// Create new quantum encoder
412    pub fn new(num_qubits: usize, depth: usize) -> Self {
413        let mut rng = thread_rng();
414        let num_params = num_qubits * depth * 3; // 3 rotations per qubit per layer
415
416        let params = Array2::from_shape_fn((depth, num_qubits * 3), |_| rng.gen_range(-PI..PI));
417
418        Self {
419            num_qubits,
420            depth,
421            params,
422        }
423    }
424
425    /// Encode quantum state
426    pub fn encode(&self, state: &Array1<Complex64>) -> QuantRS2Result<Array1<Complex64>> {
427        let mut encoded = state.clone();
428
429        // Apply parameterized layers
430        for layer in 0..self.depth {
431            // Single-qubit rotations
432            for q in 0..self.num_qubits {
433                let rx_angle = self.params[[layer, q * 3]];
434                let ry_angle = self.params[[layer, q * 3 + 1]];
435                let rz_angle = self.params[[layer, q * 3 + 2]];
436
437                encoded = self.apply_rotation(&encoded, q, rx_angle, ry_angle, rz_angle)?;
438            }
439
440            // Entangling layer (CNOT ladder)
441            for q in 0..self.num_qubits - 1 {
442                encoded = self.apply_cnot(&encoded, q, q + 1)?;
443            }
444        }
445
446        Ok(encoded)
447    }
448
449    /// Apply rotation gates
450    fn apply_rotation(
451        &self,
452        state: &Array1<Complex64>,
453        qubit: usize,
454        rx: f64,
455        ry: f64,
456        rz: f64,
457    ) -> QuantRS2Result<Array1<Complex64>> {
458        let mut result = state.clone();
459        result = self.apply_rz_gate(&result, qubit, rz)?;
460        result = self.apply_ry_gate(&result, qubit, ry)?;
461        result = self.apply_rx_gate(&result, qubit, rx)?;
462        Ok(result)
463    }
464
465    fn apply_rx_gate(
466        &self,
467        state: &Array1<Complex64>,
468        qubit: usize,
469        angle: f64,
470    ) -> QuantRS2Result<Array1<Complex64>> {
471        let dim = state.len();
472        let mut new_state = Array1::zeros(dim);
473        let cos_half = Complex64::new((angle / 2.0).cos(), 0.0);
474        let sin_half = Complex64::new(0.0, -(angle / 2.0).sin());
475
476        for i in 0..dim {
477            let j = i ^ (1 << qubit);
478            new_state[i] = state[i] * cos_half + state[j] * sin_half;
479        }
480
481        Ok(new_state)
482    }
483
484    fn apply_ry_gate(
485        &self,
486        state: &Array1<Complex64>,
487        qubit: usize,
488        angle: f64,
489    ) -> QuantRS2Result<Array1<Complex64>> {
490        let dim = state.len();
491        let mut new_state = Array1::zeros(dim);
492        let cos_half = (angle / 2.0).cos();
493        let sin_half = (angle / 2.0).sin();
494
495        for i in 0..dim {
496            let bit = (i >> qubit) & 1;
497            let j = i ^ (1 << qubit);
498            if bit == 0 {
499                new_state[i] = state[i] * cos_half - state[j] * sin_half;
500            } else {
501                new_state[i] = state[i] * cos_half + state[j] * sin_half;
502            }
503        }
504
505        Ok(new_state)
506    }
507
508    fn apply_rz_gate(
509        &self,
510        state: &Array1<Complex64>,
511        qubit: usize,
512        angle: f64,
513    ) -> QuantRS2Result<Array1<Complex64>> {
514        let dim = state.len();
515        let mut new_state = state.clone();
516        let phase = Complex64::new((angle / 2.0).cos(), -(angle / 2.0).sin());
517
518        for i in 0..dim {
519            let bit = (i >> qubit) & 1;
520            new_state[i] = if bit == 1 {
521                new_state[i] * phase
522            } else {
523                new_state[i] * phase.conj()
524            };
525        }
526
527        Ok(new_state)
528    }
529
530    /// Apply CNOT gate
531    fn apply_cnot(
532        &self,
533        state: &Array1<Complex64>,
534        control: usize,
535        target: usize,
536    ) -> QuantRS2Result<Array1<Complex64>> {
537        let dim = state.len();
538        let mut new_state = state.clone();
539
540        for i in 0..dim {
541            let control_bit = (i >> control) & 1;
542            if control_bit == 1 {
543                let j = i ^ (1 << target);
544                if i < j {
545                    let temp = new_state[i];
546                    new_state[i] = new_state[j];
547                    new_state[j] = temp;
548                }
549            }
550        }
551
552        Ok(new_state)
553    }
554
555    /// Update parameters
556    pub fn update_params(&mut self, gradients: &Array2<f64>, learning_rate: f64) {
557        self.params = &self.params - &(gradients * learning_rate);
558    }
559
560    /// Get parameters
561    pub fn params(&self) -> &Array2<f64> {
562        &self.params
563    }
564}
565
566/// Quantum contrastive learner
567#[derive(Debug, Clone)]
568pub struct QuantumContrastiveLearner {
569    /// Configuration
570    config: QuantumContrastiveConfig,
571    /// Main encoder
572    encoder: QuantumEncoder,
573    /// Momentum encoder
574    momentum_encoder: QuantumEncoder,
575    /// Augmenter
576    augmenter: QuantumAugmenter,
577}
578
579impl QuantumContrastiveLearner {
580    /// Create new quantum contrastive learner
581    pub fn new(config: QuantumContrastiveConfig) -> Self {
582        let encoder = QuantumEncoder::new(config.num_qubits, config.encoder_depth);
583        let momentum_encoder = encoder.clone();
584
585        let augmenter = QuantumAugmenter::new(
586            config.num_qubits,
587            vec![
588                QuantumAugmentation::RandomRotation,
589                QuantumAugmentation::PhaseDamping,
590            ],
591            0.1,
592        );
593
594        Self {
595            config,
596            encoder,
597            momentum_encoder,
598            augmenter,
599        }
600    }
601
602    /// Compute contrastive loss
603    pub fn contrastive_loss(
604        &self,
605        states1: &[Array1<Complex64>],
606        states2: &[Array1<Complex64>],
607    ) -> QuantRS2Result<f64> {
608        let n = states1.len();
609        if n != states2.len() {
610            return Err(QuantRS2Error::InvalidInput(
611                "Batch size mismatch".to_string(),
612            ));
613        }
614
615        // Encode all states
616        let mut z1 = Vec::with_capacity(n);
617        let mut z2 = Vec::with_capacity(n);
618
619        for i in 0..n {
620            z1.push(self.encoder.encode(&states1[i])?);
621            z2.push(self.momentum_encoder.encode(&states2[i])?);
622        }
623
624        // Compute NT-Xent loss
625        let mut total_loss = 0.0;
626
627        for i in 0..n {
628            let mut numerator = 0.0;
629            let mut denominator = 0.0;
630
631            // Positive pair fidelity
632            let pos_fidelity = self.quantum_fidelity(&z1[i], &z2[i]);
633            numerator = (pos_fidelity / self.config.temperature).exp();
634
635            // Negative pairs
636            for j in 0..n {
637                if i != j {
638                    let neg_fidelity1 = self.quantum_fidelity(&z1[i], &z2[j]);
639                    let neg_fidelity2 = self.quantum_fidelity(&z1[i], &z1[j]);
640
641                    denominator += (neg_fidelity1 / self.config.temperature).exp();
642                    denominator += (neg_fidelity2 / self.config.temperature).exp();
643                }
644            }
645
646            denominator += numerator;
647
648            total_loss -= (numerator / denominator).ln();
649        }
650
651        Ok(total_loss / n as f64)
652    }
653
654    /// Compute quantum fidelity between two states
655    fn quantum_fidelity(&self, state1: &Array1<Complex64>, state2: &Array1<Complex64>) -> f64 {
656        let mut fidelity = 0.0;
657        for (a, b) in state1.iter().zip(state2.iter()) {
658            fidelity += (a.conj() * b).norm_sqr();
659        }
660        fidelity
661    }
662
663    /// Update momentum encoder
664    pub fn update_momentum_encoder(&mut self) {
665        let main_params = self.encoder.params();
666        let mut momentum_params = self.momentum_encoder.params().clone();
667
668        // Exponential moving average
669        momentum_params =
670            &momentum_params * self.config.momentum + main_params * (1.0 - self.config.momentum);
671
672        self.momentum_encoder.params = momentum_params;
673    }
674
675    /// Train on a batch
676    pub fn train_step(
677        &mut self,
678        batch: &[Array1<Complex64>],
679        learning_rate: f64,
680    ) -> QuantRS2Result<f64> {
681        // Generate augmented views
682        let mut view1 = Vec::with_capacity(batch.len());
683        let mut view2 = Vec::with_capacity(batch.len());
684
685        for state in batch {
686            view1.push(
687                self.augmenter
688                    .augment(state, QuantumAugmentation::RandomRotation)?,
689            );
690            view2.push(
691                self.augmenter
692                    .augment(state, QuantumAugmentation::PhaseDamping)?,
693            );
694        }
695
696        // Compute loss
697        let loss = self.contrastive_loss(&view1, &view2)?;
698
699        // Compute gradients (simplified - in practice use parameter-shift rule)
700        let epsilon = 1e-4;
701        let mut gradients = Array2::zeros(self.encoder.params().dim());
702
703        for i in 0..gradients.shape()[0] {
704            for j in 0..gradients.shape()[1] {
705                // Finite difference approximation
706                let mut params_plus = self.encoder.params().clone();
707                params_plus[[i, j]] += epsilon;
708                self.encoder.params = params_plus;
709                let loss_plus = self.contrastive_loss(&view1, &view2)?;
710
711                let mut params_minus = self.encoder.params().clone();
712                params_minus[[i, j]] -= 2.0 * epsilon;
713                self.encoder.params = params_minus;
714                let loss_minus = self.contrastive_loss(&view1, &view2)?;
715
716                gradients[[i, j]] = (loss_plus - loss_minus) / (2.0 * epsilon);
717
718                // Restore params
719                let mut params_restore = self.encoder.params().clone();
720                params_restore[[i, j]] += epsilon;
721                self.encoder.params = params_restore;
722            }
723        }
724
725        // Update encoder parameters
726        self.encoder.update_params(&gradients, learning_rate);
727
728        // Update momentum encoder
729        self.update_momentum_encoder();
730
731        Ok(loss)
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738
739    #[test]
740    fn test_quantum_augmenter() {
741        let augmenter = QuantumAugmenter::new(2, vec![QuantumAugmentation::RandomRotation], 0.1);
742
743        let state = Array1::from_vec(vec![
744            Complex64::new(1.0, 0.0),
745            Complex64::new(0.0, 0.0),
746            Complex64::new(0.0, 0.0),
747            Complex64::new(0.0, 0.0),
748        ]);
749
750        let augmented = augmenter
751            .augment(&state, QuantumAugmentation::RandomRotation)
752            .unwrap();
753        assert_eq!(augmented.len(), 4);
754    }
755
756    #[test]
757    fn test_quantum_contrastive_learner() {
758        let config = QuantumContrastiveConfig {
759            num_qubits: 2,
760            encoder_depth: 2,
761            temperature: 0.5,
762            momentum: 0.999,
763            batch_size: 4,
764            num_views: 2,
765        };
766
767        let learner = QuantumContrastiveLearner::new(config);
768
769        let state = Array1::from_vec(vec![
770            Complex64::new(1.0, 0.0),
771            Complex64::new(0.0, 0.0),
772            Complex64::new(0.0, 0.0),
773            Complex64::new(0.0, 0.0),
774        ]);
775
776        let encoded = learner.encoder.encode(&state).unwrap();
777        assert_eq!(encoded.len(), 4);
778    }
779}