Skip to main content

comnum_mapping/
constellation.rs

1//! Constellations, Gray mapping and hard decisions (OptiCommPy `optic.comm.modulation`).
2//!
3//! # Constellations
4//!
5//! A [`Constellation`] stores its `M = 2^b` points **indexed by their bit label**: the
6//! point `points()[i]` carries the bits of `i` written on `b` bits, most significant
7//! bit first. The points are those of OptiCommPy, not normalised:
8//!
9//! - PAM: `{-(M-1), …, -1, 1, …, M-1}` (minimum distance 2, `E_s = (M²-1)/3`);
10//! - square QAM: the Cartesian product of two `√M`-PAM (`E_s = 2(M-1)/3`);
11//! - PSK: `e^{j2πk/M}`, `k = 0..M-1` (`E_s = 1`);
12//! - OOK: `{0, 1}`.
13//!
14//! The Gray labels are assigned as in OptiCommPy: the points are listed in their
15//! construction order (PAM and PSK in increasing order; QAM row by row from the top,
16//! every other row reversed, a "snake" that makes neighbouring points adjacent in the
17//! list), and the `i`-th point of the list gets the Gray code `i ⊕ (i >> 1)`. Two
18//! nearest neighbours then differ by one bit, and the bits of a symbol stream are those
19//! of OptiCommPy's `modulateGray`.
20//!
21//! Use [`Constellation::normalized`] for unit average energy (`E_s = 1`), the usual
22//! convention of the simulations.
23//!
24//! # Decisions
25//!
26//! [`Constellation::decide`] returns the nearest point (the maximum-likelihood decision
27//! over AWGN); the soft demapper is in [`crate::demapper`].
28//!
29//! # Examples
30//! ```
31//! use comnum_mapping::constellation::{Constellation, ConstellationKind};
32//! use comnum_math::C64;
33//! let c = Constellation::qam(16).unwrap();
34//! assert_eq!(c.points()[0], C64::new(-3.0, 3.0));            // label 0000
35//! assert!((c.energy() - 10.0).abs() < 1e-12);                 // 2(M-1)/3
36//! let x = c.map_bits(&[0, 0, 0, 1, 1, 0, 1, 1]).unwrap();
37//! assert_eq!(x, vec![c.points()[1], c.points()[11]]);
38//! // Gray mapping: nearest neighbours differ by one bit
39//! for i in 0..16 {
40//!     for j in 0..16 {
41//!         if (c.points()[i] - c.points()[j]).norm() == 2.0 {
42//!             assert_eq!((i ^ j).count_ones(), 1);
43//!         }
44//!     }
45//! }
46//! assert_eq!(Constellation::new(ConstellationKind::Psk, 8).unwrap().order(), 8);
47//! ```
48
49use comnum_core::{Error, Result, ensure, same_len};
50use comnum_math::bits::bits_to_indices;
51use comnum_math::nearest::Slicer;
52use comnum_math::{C64, cis, gray, index_to_bits};
53use std::str::FromStr;
54
55/// The family of a constellation.
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57#[non_exhaustive]
58pub enum ConstellationKind {
59    /// Pulse amplitude modulation (real).
60    Pam,
61    /// Square quadrature amplitude modulation.
62    Qam,
63    /// Phase shift keying.
64    Psk,
65    /// On-off keying `{0, 1}` (real).
66    Ook,
67    /// Points given by the user ([`Constellation::from_points`]).
68    Custom,
69}
70
71impl FromStr for ConstellationKind {
72    type Err = Error;
73
74    /// Parse `"pam"`, `"qam"`, `"psk"` or `"ook"` (the `constType` of OptiCommPy),
75    /// case-insensitive.
76    ///
77    /// # Errors
78    /// [`Error::Config`] for an unknown name.
79    fn from_str(s: &str) -> Result<ConstellationKind> {
80        match s.to_ascii_lowercase().as_str() {
81            "pam" => Ok(ConstellationKind::Pam),
82            "qam" => Ok(ConstellationKind::Qam),
83            "psk" => Ok(ConstellationKind::Psk),
84            "ook" => Ok(ConstellationKind::Ook),
85            other => Err(Error::Config(format!("unknown constellation type {other:?}: pam, qam, psk or ook"))),
86        }
87    }
88}
89
90impl ConstellationKind {
91    /// The name of the family: `"pam"`, `"qam"`, `"psk"`, `"ook"` or `"custom"` (the
92    /// names that [`ConstellationKind::from_str`] reads, but the last).
93    ///
94    /// # Examples
95    /// ```
96    /// use comnum_mapping::constellation::ConstellationKind;
97    /// let k: ConstellationKind = "QAM".parse()?;
98    /// assert_eq!(k.name(), "qam");
99    /// # Ok::<(), comnum_core::Error>(())
100    /// ```
101    pub fn name(self) -> &'static str {
102        match self {
103            ConstellationKind::Pam => "pam",
104            ConstellationKind::Qam => "qam",
105            ConstellationKind::Psk => "psk",
106            ConstellationKind::Ook => "ook",
107            ConstellationKind::Custom => "custom",
108        }
109    }
110
111    /// Whether the constellation is real (one real dimension per symbol).
112    pub fn is_real(self) -> bool {
113        matches!(self, ConstellationKind::Pam | ConstellationKind::Ook)
114    }
115}
116
117/// The largest number of bits of [`gray_code`] (16 777 216 code words; the $2^{n}$ words
118/// are allocated at once).
119pub const MAX_GRAY_BITS: u32 = 24;
120
121/// The Gray code of every `n`-bit integer: `code[i] = i ⊕ (i >> 1)` ([`comnum_math::gray`]).
122///
123/// # Errors
124/// [`Error::Config`] for more than [`MAX_GRAY_BITS`] bits.
125pub fn gray_code(n_bits: u32) -> Result<Vec<usize>> {
126    ensure(n_bits <= MAX_GRAY_BITS, || format!("n_bits must be at most {MAX_GRAY_BITS}, got {n_bits}"))?;
127    Ok((0..1usize << n_bits).map(gray).collect())
128}
129
130/// The bits of every index, `bits_per_symbol` bits each, most significant bit first
131/// ([`comnum_math::index_to_bits`]).
132pub fn indices_to_bits(indices: &[usize], bits_per_symbol: usize) -> Vec<u8> {
133    let mut out = Vec::with_capacity(indices.len() * bits_per_symbol);
134    for &i in indices {
135        out.extend(index_to_bits(i, bits_per_symbol));
136    }
137    out
138}
139
140/// The real BPSK symbols $1 - 2b$ of bits ($0 \mapsto +1$, $1 \mapsto -1$), the
141/// modulation of the code tests over a real channel ([`crate::demapper::bpsk_llr`] is
142/// its demapper).
143///
144/// # Examples
145/// ```
146/// use comnum_mapping::constellation::bpsk;
147/// assert_eq!(bpsk(&[0, 1, 1]), [1.0, -1.0, -1.0]);
148/// ```
149pub fn bpsk(bits: &[u8]) -> Vec<f64> {
150    bits.iter().map(|b| 1.0 - 2.0 * f64::from(*b)).collect()
151}
152
153/// A labelled constellation: `points()[i]` carries the bits of `i`.
154#[derive(Clone, Debug, PartialEq)]
155pub struct Constellation {
156    kind: ConstellationKind,
157    points: Vec<C64>,
158    bits: usize,
159    slicer: Slicer,
160}
161
162impl Constellation {
163    /// The constellation of a family and an order `m` (power of two; a square for QAM;
164    /// 2 for OOK).
165    ///
166    /// # Errors
167    /// [`Error::Config`] unless `m` is a power of two ≥ 2 (a square for QAM).
168    pub fn new(kind: ConstellationKind, m: usize) -> Result<Constellation> {
169        ensure(m >= 2 && m.is_power_of_two(), || format!("the order must be a power of two ≥ 2, got {m}"))?;
170        let natural: Vec<C64> = match kind {
171            ConstellationKind::Pam => pam_levels(m).into_iter().map(|v| C64::new(v, 0.0)).collect(),
172            ConstellationKind::Qam => {
173                let l = qam_side(m).ok_or_else(|| Error::Config(format!("QAM needs a square order, got {m}")))?;
174                let lv = pam_levels(l);
175                let mut pts = Vec::with_capacity(m);
176                for r in 0..l {
177                    let im = lv[l - 1 - r];
178                    let row: Vec<C64> = lv.iter().map(|&re| C64::new(re, im)).collect();
179                    if r % 2 == 1 {
180                        pts.extend(row.into_iter().rev());
181                    } else {
182                        pts.extend(row);
183                    }
184                }
185                pts
186            }
187            ConstellationKind::Psk => (0..m).map(|k| cis(2.0 * std::f64::consts::PI * k as f64 / m as f64)).collect(),
188            ConstellationKind::Ook => {
189                ensure(m == 2, || format!("OOK has two points, got {m}"))?;
190                vec![C64::new(0.0, 0.0), C64::new(1.0, 0.0)]
191            }
192            ConstellationKind::Custom => {
193                return Err(Error::Config("use Constellation::from_points for a custom constellation".into()));
194            }
195        };
196        let bits = m.trailing_zeros() as usize;
197        let mut points = vec![C64::new(0.0, 0.0); m];
198        for (i, p) in natural.into_iter().enumerate() {
199            points[gray(i)] = p;
200        }
201        Ok(Constellation::assemble(kind, points, bits))
202    }
203
204    fn assemble(kind: ConstellationKind, points: Vec<C64>, bits: usize) -> Constellation {
205        let slicer = Slicer::new(&points);
206        Constellation { kind, points, bits, slicer }
207    }
208
209    /// Square `m`-QAM (`m` = 4, 16, 64, 256, …).
210    ///
211    /// # Errors
212    /// [`Error::Config`] unless `m` is an even power of two ≥ 4.
213    pub fn qam(m: usize) -> Result<Constellation> {
214        Constellation::new(ConstellationKind::Qam, m)
215    }
216
217    /// `m`-PSK.
218    ///
219    /// # Errors
220    /// [`Error::Config`] unless `m` is a power of two ≥ 2.
221    pub fn psk(m: usize) -> Result<Constellation> {
222        Constellation::new(ConstellationKind::Psk, m)
223    }
224
225    /// `m`-PAM.
226    ///
227    /// # Errors
228    /// [`Error::Config`] unless `m` is a power of two ≥ 2.
229    pub fn pam(m: usize) -> Result<Constellation> {
230        Constellation::new(ConstellationKind::Pam, m)
231    }
232
233    /// On-off keying.
234    pub fn ook() -> Constellation {
235        Constellation::new(ConstellationKind::Ook, 2).unwrap_or_else(|_| unreachable!("OOK of order 2 is valid"))
236    }
237
238    /// The constellation of a name: `16-QAM`, `qam16`, `QPSK` (4-QAM), `BPSK` (2-PSK),
239    /// `8-PSK`, `4-PAM`, `OOK` (case-insensitive; `-`, `_` and spaces are ignored), not
240    /// normalised (call [`Constellation::normalized`] for unit energy).
241    ///
242    /// # Errors
243    /// [`Error::Config`] for an unknown family, a missing order or an invalid one.
244    ///
245    /// # Examples
246    /// ```
247    /// use comnum_mapping::constellation::{Constellation, ConstellationKind};
248    /// let c = Constellation::from_name("16-QAM")?;
249    /// assert_eq!((c.kind(), c.order()), (ConstellationKind::Qam, 16));
250    /// assert_eq!(Constellation::from_name("qpsk")?, Constellation::qam(4)?);
251    /// assert!(Constellation::from_name("sixteen").is_err());
252    /// # Ok::<(), comnum_core::Error>(())
253    /// ```
254    pub fn from_name(name: &str) -> Result<Constellation> {
255        let t = name.to_ascii_lowercase().replace(['-', '_', ' '], "");
256        let (kind, m) = match t.as_str() {
257            "qpsk" => (ConstellationKind::Qam, 4),
258            "bpsk" => (ConstellationKind::Psk, 2),
259            "ook" => (ConstellationKind::Ook, 2),
260            _ => {
261                let digits: String = t.chars().filter(char::is_ascii_digit).collect();
262                let letters: String = t.chars().filter(|c| !c.is_ascii_digit()).collect();
263                let m = digits
264                    .parse::<usize>()
265                    .map_err(|_| Error::Config(format!("cannot read the constellation {name:?}")))?;
266                (letters.parse()?, m)
267            }
268        };
269        Constellation::new(kind, m)
270    }
271
272    /// A constellation from its points, `points[i]` labelled by the bits of `i`.
273    ///
274    /// # Errors
275    /// [`Error::Config`] unless the number of points is a power of two ≥ 2.
276    pub fn from_points(points: Vec<C64>) -> Result<Constellation> {
277        let m = points.len();
278        ensure(m >= 2 && m.is_power_of_two(), || format!("the number of points must be a power of two ≥ 2, got {m}"))?;
279        Ok(Constellation::assemble(ConstellationKind::Custom, points, m.trailing_zeros() as usize))
280    }
281
282    /// The same points as [`Constellation::new`] with a set-partitioning labelling
283    /// (Ungerboeck 1982): fixing the last bits of the label leaves subsets whose minimum
284    /// distance grows. PSK gets the natural labelling, the point $e^{j2\pi k/M}$ carrying
285    /// the bits of $k$; 16-QAM the labelling of Caire, Taricco & Biglieri (*IEEE Trans.
286    /// Inf. Theory* 44(3), 1998, Fig. 2(a)). The family of the result is
287    /// [`ConstellationKind::Custom`].
288    ///
289    /// # Errors
290    /// [`Error::Config`] for another family or order.
291    ///
292    /// # Examples
293    /// ```
294    /// use comnum_mapping::constellation::{Constellation, ConstellationKind};
295    /// use comnum_math::C64;
296    /// let c = Constellation::set_partitioning(ConstellationKind::Qam, 16)?;
297    /// assert_eq!(c.points()[0b1001], C64::new(-3.0, 3.0));
298    /// // the points ending in 00 form a 4-QAM of side 4
299    /// let sub: Vec<C64> = (0..16).filter(|i| i & 3 == 0).map(|i| c.points()[i]).collect();
300    /// assert!(sub.iter().all(|a| sub.iter().all(|b| a == b || (a - b).norm() >= 4.0)));
301    /// # Ok::<(), comnum_core::Error>(())
302    /// ```
303    pub fn set_partitioning(kind: ConstellationKind, m: usize) -> Result<Constellation> {
304        let points = match (kind, m) {
305            (ConstellationKind::Psk, _) => {
306                ensure(m >= 2 && m.is_power_of_two(), || format!("the order must be a power of two ≥ 2, got {m}"))?;
307                (0..m).map(|k| cis(2.0 * std::f64::consts::PI * k as f64 / m as f64)).collect()
308            }
309            (ConstellationKind::Qam, 16) => {
310                // Caire et al. 1998, Fig. 2(a), row by row from the top, left to right
311                let rows: [[usize; 4]; 4] = [
312                    [0b1001, 0b1100, 0b1101, 0b1000],
313                    [0b1110, 0b1011, 0b1010, 0b1111],
314                    [0b0101, 0b0000, 0b0001, 0b0100],
315                    [0b0010, 0b0111, 0b0110, 0b0011],
316                ];
317                let mut pts = vec![C64::new(0.0, 0.0); 16];
318                for (r, row) in rows.iter().enumerate() {
319                    for (c, &label) in row.iter().enumerate() {
320                        pts[label] = C64::new(2.0 * c as f64 - 3.0, 3.0 - 2.0 * r as f64);
321                    }
322                }
323                pts
324            }
325            _ => return Err(Error::Config(format!("no set-partitioning labelling for {}-{}", m, kind.name()))),
326        };
327        Constellation::from_points(points)
328    }
329
330    /// The family.
331    pub fn kind(&self) -> ConstellationKind {
332        self.kind
333    }
334
335    /// The number of points `M`.
336    pub fn order(&self) -> usize {
337        self.points.len()
338    }
339
340    /// Bits per symbol `log2 M`.
341    pub fn bits_per_symbol(&self) -> usize {
342        self.bits
343    }
344
345    /// The points, indexed by their label.
346    pub fn points(&self) -> &[C64] {
347        &self.points
348    }
349
350    /// Average energy `Σ |x_i|² / M` (uniform probabilities).
351    pub fn energy(&self) -> f64 {
352        comnum_math::power(&self.points)
353    }
354
355    /// Average energy `Σ p_i |x_i|²` under the probabilities `px`.
356    ///
357    /// # Errors
358    /// [`Error::Shape`] unless `px` holds one probability per point.
359    pub fn energy_with(&self, px: &[f64]) -> Result<f64> {
360        same_len(px.len(), self.points.len(), "energy_with: probabilities and points")?;
361        Ok(self.points.iter().zip(px).map(|(p, w)| w * p.norm_sqr()).sum())
362    }
363
364    /// The prior probabilities of the points: `px` checked (one non-negative value per
365    /// point, not all zero) and normalised to a sum of 1, or uniform if `None`.
366    ///
367    /// # Errors
368    /// [`Error::Shape`] for another number of values, [`Error::Config`] for negative or
369    /// all-zero values.
370    pub fn priors(&self, px: Option<&[f64]>) -> Result<Vec<f64>> {
371        let m = self.order();
372        Ok(match px {
373            None => vec![1.0 / m as f64; m],
374            Some(p) => {
375                same_len(p.len(), m, "probabilities and constellation points")?;
376                let total: f64 = p.iter().sum();
377                ensure(total > 0.0 && p.iter().all(|v| *v >= 0.0), || "the probabilities must be non-negative".into())?;
378                p.iter().map(|v| v / total).collect()
379            }
380        })
381    }
382
383    /// The same constellation scaled to unit average energy under the probabilities
384    /// `px` (probabilistic shaping), normalised to a sum of 1 first as by
385    /// [`Constellation::priors`].
386    ///
387    /// # Errors
388    /// As [`Constellation::priors`]; [`Error::Config`] if the energy under `px` is zero.
389    pub fn normalized_under(&self, px: &[f64]) -> Result<Constellation> {
390        let es = self.energy_with(&self.priors(Some(px))?)?;
391        ensure(es > 0.0, || "the constellation has no energy".into())?;
392        Ok(self.scaled(1.0 / es.sqrt()))
393    }
394
395    /// The same constellation scaled to unit average energy.
396    pub fn normalized(&self) -> Constellation {
397        self.scaled(1.0 / self.energy().sqrt())
398    }
399
400    /// The same constellation with every point multiplied by `s`.
401    pub fn scaled(&self, s: f64) -> Constellation {
402        Constellation::assemble(self.kind, self.points.iter().map(|p| p * s).collect(), self.bits)
403    }
404
405    /// The label (index) of every symbol of a bit stream (most significant bit first).
406    ///
407    /// # Errors
408    /// [`Error::Shape`] if the number of bits is not a multiple of `log2 M`.
409    pub fn indices(&self, bits: &[u8]) -> Result<Vec<usize>> {
410        bits_to_indices(bits, self.bits)
411    }
412
413    /// Map a bit stream to symbols (OptiCommPy `modulateGray`).
414    ///
415    /// # Errors
416    /// [`Error::Shape`] if the number of bits is not a multiple of `log2 M`.
417    pub fn map_bits(&self, bits: &[u8]) -> Result<Vec<C64>> {
418        Ok(self.indices(bits)?.into_iter().map(|i| self.points[i]).collect())
419    }
420
421    /// The symbols of a sequence of labels.
422    pub fn symbols(&self, indices: &[usize]) -> Vec<C64> {
423        indices.iter().map(|&i| self.points[i]).collect()
424    }
425
426    /// The label of the nearest point of every sample (minimum Euclidean distance).
427    pub fn decide(&self, z: &[C64]) -> Vec<usize> {
428        z.iter().map(|&v| self.nearest(v)).collect()
429    }
430
431    /// The label of the point nearest to `z`.
432    #[inline]
433    pub fn nearest(&self, z: C64) -> usize {
434        self.slicer.nearest(z)
435    }
436
437    /// The nearest point of every sample.
438    pub fn project(&self, z: &[C64]) -> Vec<C64> {
439        z.iter().map(|&v| self.points[self.nearest(v)]).collect()
440    }
441
442    /// Hard-decision bits of every sample (OptiCommPy `demodulateGray`).
443    pub fn demap_bits(&self, z: &[C64]) -> Vec<u8> {
444        indices_to_bits(&self.decide(z), self.bits)
445    }
446}
447
448/// `{-(m-1), …, -1, 1, …, m-1}`.
449fn pam_levels(m: usize) -> Vec<f64> {
450    (0..m).map(|i| 2.0 * i as f64 - (m as f64 - 1.0)).collect()
451}
452
453/// The floor of a prior probability before its logarithm: $\ln 10^{-300} \approx -691$.
454pub const PRIOR_FLOOR: f64 = 1e-300;
455
456/// The log of prior probabilities, floored at [`PRIOR_FLOOR`] so that a zero
457/// probability gives a finite log (the metric of an impossible point).
458pub fn log_priors(px: &[f64]) -> Vec<f64> {
459    px.iter().map(|v| v.max(PRIOR_FLOOR).ln()).collect()
460}
461
462/// The side of a square QAM of order `m` (`None` unless `m` is a perfect square).
463pub fn qam_side(m: usize) -> Option<usize> {
464    let side = (m as f64).sqrt().round() as usize;
465    (side * side == m).then_some(side)
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471
472    #[test]
473    fn energy_with_rejects_too_few_probabilities_instead_of_truncating() {
474        assert!(matches!(Constellation::qam(4).unwrap().energy_with(&[0.5, 0.5]), Err(Error::Shape(_))));
475    }
476
477    #[test]
478    fn normalized_under_treats_unnormalised_priors_as_their_normalisation() {
479        let c = Constellation::qam(16).unwrap();
480        let p: Vec<f64> = c.points().iter().map(|x| (-0.1 * x.norm_sqr()).exp()).collect();
481        let total: f64 = p.iter().sum();
482        let unit: Vec<f64> = p.iter().map(|v| v / total).collect();
483        let a = c.normalized_under(&p).unwrap();
484        let b = c.normalized_under(&unit).unwrap();
485        assert!(a.points().iter().zip(b.points()).all(|(x, y)| (x - y).norm() < 1e-14));
486        assert!((a.energy_with(&unit).unwrap() - 1.0).abs() < 1e-14);
487        // three times the priors: the same constellation, not one of energy 1/3
488        let triple: Vec<f64> = unit.iter().map(|v| 3.0 * v).collect();
489        assert!((c.normalized_under(&triple).unwrap().energy_with(&unit).unwrap() - 1.0).abs() < 1e-14);
490        assert!(matches!(c.normalized_under(&unit[..15]), Err(Error::Shape(_))));
491        assert!(matches!(c.normalized_under(&[-1.0; 16]), Err(Error::Config(_))));
492    }
493
494    #[test]
495    fn the_grid_slicer_agrees_with_the_exhaustive_search() {
496        let mut rng = comnum_core::rng::Rng::new(11);
497        let cs = [
498            Constellation::qam(4).unwrap(),
499            Constellation::qam(16).unwrap().normalized(),
500            Constellation::qam(256).unwrap().scaled(0.37),
501            Constellation::pam(8).unwrap(),
502            Constellation::psk(8).unwrap(),
503            Constellation::from_points(vec![
504                C64::new(0.0, 0.0),
505                C64::new(1.0, 0.2),
506                C64::new(-0.4, 1.0),
507                C64::new(0.3, -0.9),
508            ])
509            .unwrap(),
510        ];
511        let grids = [true, true, true, true, false, false];
512        for (c, grid) in cs.iter().zip(grids) {
513            assert_eq!(c.slicer.is_grid(), grid, "{:?} {}", c.kind(), c.order());
514            let brute = Slicer::exhaustive(c.points());
515            let scale = c.energy().sqrt() * 1.5;
516            for _ in 0..2000 {
517                let z = rng.complex_gaussian() * scale;
518                assert_eq!(c.nearest(z), brute.nearest(z), "{:?} {} at {z}", c.kind(), c.order());
519            }
520        }
521    }
522
523    #[test]
524    fn energies_and_labels() {
525        assert!((Constellation::pam(4).unwrap().energy() - 5.0).abs() < 1e-12);
526        assert!((Constellation::qam(64).unwrap().energy() - 42.0).abs() < 1e-12);
527        assert!((Constellation::psk(8).unwrap().energy() - 1.0).abs() < 1e-12);
528        assert!(Constellation::qam(32).is_err());
529        let c = Constellation::psk(4).unwrap();
530        // natural order 1, j, -1, -j gets the Gray codes 00, 01, 11, 10
531        assert!((c.points()[0b01] - C64::new(0.0, 1.0)).norm() < 1e-12);
532        assert!((c.points()[0b11] - C64::new(-1.0, 0.0)).norm() < 1e-12);
533    }
534}