orion_sdr/modulate/ofdm.rs
1// Copyright (c) 2025-2026 G & R Associates LLC
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4// src/modulate/ofdm.rs
5use super::bpsk::BpskMapper;
6use super::qam::{Qam16Mapper, Qam64Mapper, Qam256Mapper, QamMapper};
7use super::qpsk::QpskMapper;
8use crate::core::{Block, WorkReport};
9use crate::dsp::Rotator;
10use crate::fec::{
11 CrcKind, DecodeRule, HeaderFormat, InnerFec, InterleaverKind, OuterFec, ScramblerKind,
12 ScramblerPos, SeedMode,
13};
14use crate::multicarrier::{
15 CarrierGrid, CarrierPlan, CyclicPrefixInsert, GridMap, IfftBlock, TxLowpass,
16};
17use num_complex::Complex32 as C32;
18
19/// Constellation order used by an OFDM data carrier's symbol mapper.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ConstellationOrder {
22 Bpsk,
23 Qpsk,
24 Qam16,
25 Qam64,
26 Qam256,
27}
28
29impl ConstellationOrder {
30 pub fn bits_per_symbol(self) -> usize {
31 match self {
32 ConstellationOrder::Bpsk => 1,
33 ConstellationOrder::Qpsk => 2,
34 ConstellationOrder::Qam16 => 4,
35 ConstellationOrder::Qam64 => 6,
36 ConstellationOrder::Qam256 => 8,
37 }
38 }
39}
40
41/// OFDM waveform configuration: the resource grid ([`CarrierPlan`]) plus the
42/// sample rate, RF/IF carrier, output gain, and data-carrier constellation
43/// order shared by the transmitter ([`OfdmMod`]) and receiver.
44///
45/// Numerology (`n_fft`, `cp_len`, carrier layout) is caller-owned and lives
46/// in `carrier_plan`; the library bakes in no standard's spacing or CP length
47/// (see the numerology guidance in `docs/design.md`). `rf_hz == 0.0` selects
48/// baseband output; any nonzero value upconverts via a `Rotator`.
49///
50/// The frame-layer fields (`outer_fec`, `inner_fec`, the two interleavers,
51/// `header_format`, the two CRCs, `scrambler`, `scrambler_pos`) default to
52/// "absent" and are set with the `with_*` builder methods, so the positional
53/// [`OfdmConfig::new`] and the bare `OfdmMod`/`OfdmDemod` symbol pipeline are
54/// unaffected by them. They configure the concatenated COFDM coding chain used
55/// by the OFDM frame modulator/demodulator (see `modulate::ofdm_frame`).
56#[derive(Debug, Clone, PartialEq)]
57pub struct OfdmConfig {
58 pub carrier_plan: CarrierPlan,
59 pub fs: f32,
60 pub rf_hz: f32,
61 pub gain: f32,
62 pub constellation: ConstellationOrder,
63 // ── Frame-layer (COFDM) configuration ──
64 pub outer_fec: OuterFec,
65 pub inner_fec: InnerFec,
66 pub outer_interleaver: InterleaverKind,
67 pub inner_interleaver: InterleaverKind,
68 pub header_format: HeaderFormat,
69 pub payload_crc: CrcKind,
70 pub header_crc: CrcKind,
71 pub scrambler: ScramblerKind,
72 pub scrambler_pos: ScramblerPos,
73 /// Check-node rule the receiver's LDPC inner decoder uses.
74 /// [`DecodeRule::SumProduct`] (the default) is exact belief propagation;
75 /// [`DecodeRule::ScaledMinSum`] trades ≲0.3 dB of coding gain for ~2×
76 /// decode throughput (see the R8a investigation in `docs/performance.md`).
77 /// TX-only paths ignore this.
78 pub ldpc_decode_rule: DecodeRule,
79 /// When set, the frame layer maps/demaps payload symbols through DVB-T's
80 /// four-phase **scattered-pilot** grid rotation instead of the single static
81 /// grid in `carrier_plan` (see `waveform::dvb_t`). The `carrier_plan` still
82 /// describes the representative phase-0 grid (its 1512 data carriers drive
83 /// all the count-based bookkeeping); the physical pilot/data bins rotate per
84 /// symbol underneath. Only valid for a 2K DVB-T plan. Defaults to `false`,
85 /// so every non-DVB-T link is unaffected.
86 pub dvb_t_scattered: bool,
87 /// Receiver FFT-window back-off in samples: how far the demodulator pulls
88 /// its `n_fft`-sample window *earlier* from the cyclic-prefix boundary into
89 /// the guard interval (clamped to `cp_len` at use). `0` (the default) is the
90 /// standard CP-boundary window. A positive value leaves guard on both sides
91 /// of the useful part — receiver practice for multipath/pre-echo robustness,
92 /// and the enabler for RX-transparent TX symbol windowing. **RX-only:**
93 /// TX paths ignore this, so on-air output is unaffected.
94 ///
95 /// **Requires an equalizer.** Sliding the window by `b` multiplies every
96 /// subcarrier by a linear phase ramp `exp(-j2πkb/n_fft)` (FFT shift
97 /// theorem). This is transparent only on the *equalized* path (the streaming
98 /// demod, or the DVB-T scattered path), where the training/pilot estimate is
99 /// measured at the same back-off and divides the ramp back out. On a bare,
100 /// unequalized demod (`OfdmDemod` / batch `OfdmFrameDemod` with no channel
101 /// estimate) a nonzero back-off leaves the ramp uncorrected and corrupts the
102 /// decode — leave it `0` there.
103 pub rx_window_backoff: usize,
104 /// Optional TX baseband low-pass (spectral mask) applied by the frame
105 /// modulator across the **assembled** stream, after CP insertion and any
106 /// symbol windowing. `None` (the default) leaves the on-air output
107 /// unchanged.
108 ///
109 /// **TX-only field**, but not RX-indifferent: the filter is a linear
110 /// channel the pilot/training equalizer absorbs, so no *decoding* change is
111 /// needed, yet its group delay must land in guard the receiver discards.
112 /// Pair it with [`rx_window_backoff`](Self::rx_window_backoff) — the same
113 /// knob symbol windowing uses — and keep
114 /// `roll_off + group_delay ≤ min(cp_len − backoff, backoff)`
115 /// ([`TxLowpass::fits_guard`]).
116 pub tx_lowpass: Option<TxLowpass>,
117}
118
119/// Rejects an [`OfdmConfig`] whose frame-layer settings are mutually
120/// inconsistent.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
122pub enum FrameConfigError {
123 /// A per-frame-random scrambler seed has no way to reach the receiver when
124 /// there is no in-band header to carry it.
125 #[error("per-frame-random scrambler seed requires a header (header_format != NoHeader)")]
126 PerFrameSeedNeedsHeader,
127 /// A block interleaver was requested with a zero dimension.
128 #[error("block interleaver dimensions must be nonzero")]
129 ZeroInterleaverDim,
130 /// A BCH outer code was requested with t = 0 (no correction).
131 #[error("BCH outer code requires t >= 1")]
132 ZeroBchT,
133 /// A Reed–Solomon outer code has invalid dimensions.
134 #[error("Reed–Solomon requires 0 < n_parity < n <= 255 with n_parity even")]
135 BadRsConfig,
136}
137
138impl OfdmConfig {
139 pub fn new(
140 carrier_plan: CarrierPlan,
141 fs: f32,
142 rf_hz: f32,
143 gain: f32,
144 constellation: ConstellationOrder,
145 ) -> Self {
146 Self {
147 carrier_plan,
148 fs,
149 rf_hz,
150 gain,
151 constellation,
152 outer_fec: OuterFec::None,
153 inner_fec: InnerFec::None,
154 outer_interleaver: InterleaverKind::None,
155 inner_interleaver: InterleaverKind::None,
156 header_format: HeaderFormat::OrionSdr,
157 payload_crc: CrcKind::Crc32,
158 header_crc: CrcKind::Crc16,
159 scrambler: ScramblerKind::None,
160 scrambler_pos: ScramblerPos::BeforeOuterFec,
161 ldpc_decode_rule: DecodeRule::SumProduct,
162 dvb_t_scattered: false,
163 rx_window_backoff: 0,
164 tx_lowpass: None,
165 }
166 }
167
168 /// Sets the sample rate (S/s). A generic builder — e.g. a DVB-T caller
169 /// selects a narrowband bandwidth mode with
170 /// `cfg.with_fs(NbBandwidth::Bw1MHz.fs())`.
171 pub fn with_fs(mut self, fs: f32) -> Self {
172 self.fs = fs;
173 self
174 }
175
176 pub fn with_outer_fec(mut self, outer_fec: OuterFec) -> Self {
177 self.outer_fec = outer_fec;
178 self
179 }
180
181 pub fn with_inner_fec(mut self, inner_fec: InnerFec) -> Self {
182 self.inner_fec = inner_fec;
183 self
184 }
185
186 pub fn with_outer_interleaver(mut self, il: InterleaverKind) -> Self {
187 self.outer_interleaver = il;
188 self
189 }
190
191 pub fn with_inner_interleaver(mut self, il: InterleaverKind) -> Self {
192 self.inner_interleaver = il;
193 self
194 }
195
196 pub fn with_header_format(mut self, header_format: HeaderFormat) -> Self {
197 self.header_format = header_format;
198 self
199 }
200
201 pub fn with_payload_crc(mut self, crc: CrcKind) -> Self {
202 self.payload_crc = crc;
203 self
204 }
205
206 pub fn with_header_crc(mut self, crc: CrcKind) -> Self {
207 self.header_crc = crc;
208 self
209 }
210
211 pub fn with_scrambler(mut self, scrambler: ScramblerKind) -> Self {
212 self.scrambler = scrambler;
213 self
214 }
215
216 pub fn with_scrambler_pos(mut self, pos: ScramblerPos) -> Self {
217 self.scrambler_pos = pos;
218 self
219 }
220
221 /// Selects the LDPC inner-decoder check-node rule (receiver side). Defaults
222 /// to [`DecodeRule::SumProduct`]; pass [`DecodeRule::ScaledMinSum`] (α ≈
223 /// 0.75) for ~2× decode throughput at a ≲0.3 dB coding-gain cost.
224 pub fn with_ldpc_decode_rule(mut self, rule: DecodeRule) -> Self {
225 self.ldpc_decode_rule = rule;
226 self
227 }
228
229 /// Enables DVB-T four-phase scattered-pilot grid rotation in the frame layer
230 /// (see [`dvb_t_scattered`](Self::dvb_t_scattered)). The `carrier_plan` must
231 /// be a 2K DVB-T phase-0 plan (1512 data carriers).
232 pub fn with_dvb_t_scattered(mut self, scattered: bool) -> Self {
233 self.dvb_t_scattered = scattered;
234 self
235 }
236
237 /// Sets the receiver FFT-window back-off in samples (see
238 /// [`rx_window_backoff`](Self::rx_window_backoff)). RX-only; TX output is
239 /// unaffected. Clamped to `cp_len` where the window is selected.
240 pub fn with_rx_window_backoff(mut self, backoff: usize) -> Self {
241 self.rx_window_backoff = backoff;
242 self
243 }
244
245 /// Enables TX symbol windowing with a `roll_off`-sample raised-cosine taper
246 /// per symbol edge (see
247 /// [`CarrierPlan::with_window_roll_off`](crate::multicarrier::CarrierPlan::with_window_roll_off)).
248 /// `0` disables it (the default). The taper reduces out-of-band emission but
249 /// is only RX-transparent when paired with a compatible
250 /// [`rx_window_backoff`](Self::rx_window_backoff) (`roll_off ≤ cp_len/2` with
251 /// back-off `cp_len/2` is the transparent operating point).
252 ///
253 /// See [`with_symbol_window_beta_guard`](Self::with_symbol_window_beta_guard)
254 /// and [`with_symbol_window_beta_tu`](Self::with_symbol_window_beta_tu) to
255 /// specify the roll-off as a fraction instead of raw samples.
256 pub fn with_symbol_window(mut self, roll_off: usize) -> Self {
257 self.carrier_plan = self.carrier_plan.with_window_roll_off(roll_off);
258 self
259 }
260
261 /// Enables TX symbol windowing with a roll-off given as a fraction of the
262 /// **guard** (cyclic prefix): `roll_off = round(beta * cp_len)`. `beta` in
263 /// `0.0..=0.5`; `beta = 0.5` is the maximum RX-transparent taper
264 /// (`roll_off = cp_len/2`, paired with `rx_window_backoff = cp_len/2`). This
265 /// convention makes the transparency budget explicit, since the taper is
266 /// bounded by half the guard. Clamped to `[0, 0.5]`.
267 pub fn with_symbol_window_beta_guard(self, beta: f32) -> Self {
268 let cp_len = self.carrier_plan.cp_len();
269 let roll_off = (beta.clamp(0.0, 0.5) * cp_len as f32).round() as usize;
270 self.with_symbol_window(roll_off)
271 }
272
273 /// Enables TX symbol windowing with a roll-off given as a fraction of the
274 /// **useful symbol** `Tu` (`n_fft`): `roll_off = round(beta * n_fft)` — the
275 /// convention used by DVB-family windowing tables (which express roll-offs
276 /// relative to `Tu`). Note the resulting `roll_off` must still satisfy the
277 /// transparency bound `roll_off ≤ cp_len/2` for a matched back-off to keep
278 /// the decode transparent; a larger `beta` shapes the spectrum more but is
279 /// only transparent if the guard is long enough. Clamped so `2*roll_off` does
280 /// not exceed the symbol length.
281 pub fn with_symbol_window_beta_tu(self, beta: f32) -> Self {
282 let n_fft = self.carrier_plan.n_fft();
283 let roll_off = (beta.max(0.0) * n_fft as f32).round() as usize;
284 self.with_symbol_window(roll_off)
285 }
286
287 /// Enables the TX baseband low-pass (spectral mask) applied by
288 /// [`OfdmFrameMod::modulate_frame`](crate::modulate::OfdmFrameMod::modulate_frame)
289 /// across the assembled stream (see [`tx_lowpass`](Self::tx_lowpass)).
290 /// Off by default.
291 ///
292 /// Unlike symbol windowing this is *not* bounded by the windowing ceiling —
293 /// it attenuates the skirt directly in the frequency domain, so its gain
294 /// stacks on top. It changes nothing about how the receiver *decodes*, but
295 /// its group delay shares the guard budget with any symbol taper and needs
296 /// a nonzero [`rx_window_backoff`](Self::rx_window_backoff) to land in:
297 /// check [`TxLowpass::fits_guard`].
298 pub fn with_tx_lowpass(mut self, lowpass: TxLowpass) -> Self {
299 self.tx_lowpass = Some(lowpass);
300 self
301 }
302
303 /// Convenience form of [`with_tx_lowpass`](Self::with_tx_lowpass) that reads
304 /// the occupied band edge straight off the carrier plan and centres the
305 /// filter's transition in the unoccupied band above it
306 /// ([`TxLowpass::for_null_band`]). `num_taps` stays the caller's choice
307 /// because it is what the cyclic-prefix budget constrains;
308 /// [`TxLowpass::taps_for_null_band`] suggests a length.
309 pub fn with_tx_lowpass_null_band(self, num_taps: usize, stopband_db: f32) -> Self {
310 let lowpass = TxLowpass::for_null_band(
311 self.carrier_plan.n_fft(),
312 self.carrier_plan.occupied_half_carriers(),
313 num_taps,
314 stopband_db,
315 );
316 self.with_tx_lowpass(lowpass)
317 }
318
319 /// Validates the frame-layer configuration. Returns `Ok(())` for the bare
320 /// (no-FEC, no-frame) defaults.
321 pub fn validate(&self) -> Result<(), FrameConfigError> {
322 // A per-frame-random seed needs a header block to carry it to the RX.
323 // Only OrionSdr has one; NoHeader and DvbTps do not (DvbTps signals via
324 // TPS, which does not carry a scrambler seed).
325 if let ScramblerKind::Additive {
326 seed: SeedMode::PerFrameRandom,
327 ..
328 } = self.scrambler
329 && !self.header_format.has_header_block()
330 {
331 return Err(FrameConfigError::PerFrameSeedNeedsHeader);
332 }
333 for il in [self.outer_interleaver, self.inner_interleaver] {
334 match il {
335 InterleaverKind::Block { rows, cols } if rows == 0 || cols == 0 => {
336 return Err(FrameConfigError::ZeroInterleaverDim);
337 }
338 InterleaverKind::Convolutional { branches, depth }
339 if branches == 0 || depth == 0 =>
340 {
341 return Err(FrameConfigError::ZeroInterleaverDim);
342 }
343 _ => {}
344 }
345 }
346 if let OuterFec::Bch { t } = self.outer_fec
347 && t == 0
348 {
349 return Err(FrameConfigError::ZeroBchT);
350 }
351 if let OuterFec::ReedSolomon { n, n_parity } = self.outer_fec
352 && (n == 0 || n > 255 || n_parity == 0 || n_parity >= n || n_parity % 2 != 0)
353 {
354 return Err(FrameConfigError::BadRsConfig);
355 }
356 Ok(())
357 }
358
359 pub fn bits_per_ofdm_symbol(&self) -> usize {
360 self.carrier_plan.data_carriers().len() * self.constellation.bits_per_symbol()
361 }
362
363 pub fn samples_per_ofdm_symbol(&self) -> usize {
364 self.carrier_plan.n_fft() + self.carrier_plan.cp_len()
365 }
366}
367
368/// Dispatches to the existing per-order symbol mappers (reused verbatim, not
369/// reimplemented) via a plain `match` — no `dyn` dispatch in the hot loop.
370///
371/// `pub(crate)` so `demodulate::ofdm` can reuse it to compute EVM (mapping
372/// hard-decided bits back to their ideal constellation points) without
373/// duplicating the per-order dispatch.
374pub(crate) enum MapperKind {
375 Bpsk(BpskMapper),
376 Qpsk(QpskMapper),
377 Qam16(Qam16Mapper),
378 Qam64(Qam64Mapper),
379 Qam256(Qam256Mapper),
380}
381
382impl MapperKind {
383 fn new(order: ConstellationOrder) -> Self {
384 match order {
385 ConstellationOrder::Bpsk => MapperKind::Bpsk(BpskMapper::new()),
386 ConstellationOrder::Qpsk => MapperKind::Qpsk(QpskMapper::new()),
387 ConstellationOrder::Qam16 => MapperKind::Qam16(QamMapper::new()),
388 ConstellationOrder::Qam64 => MapperKind::Qam64(QamMapper::new()),
389 ConstellationOrder::Qam256 => MapperKind::Qam256(QamMapper::new()),
390 }
391 }
392
393 #[inline(always)]
394 pub(crate) fn process(&mut self, input: &[u8], output: &mut [C32]) -> WorkReport {
395 match self {
396 MapperKind::Bpsk(m) => m.process(input, output),
397 MapperKind::Qpsk(m) => m.process(input, output),
398 MapperKind::Qam16(m) => m.process(input, output),
399 MapperKind::Qam64(m) => m.process(input, output),
400 MapperKind::Qam256(m) => m.process(input, output),
401 }
402 }
403}
404
405/// Constructs the ideal-symbol mapper for `order`, for crate-internal reuse
406/// (e.g. EVM computation in `demodulate::ofdm`).
407pub(crate) fn ideal_symbol_mapper(order: ConstellationOrder) -> MapperKind {
408 MapperKind::new(order)
409}
410
411/// OFDM transmitter: `u8` bits → `C32` IQ.
412///
413/// Pipeline: bits → symbol mapper (BPSK/QPSK/QAM, order given by
414/// `OfdmConfig::constellation`) → [`GridMap`] → [`IfftBlock`] →
415/// [`CyclicPrefixInsert`] → optional [`Rotator`] (`rf_hz == 0.0` ⇒ baseband
416/// passthrough, exactly like `BpskMod`).
417///
418/// Consumes whole `bits_per_ofdm_symbol()`-sized bit chunks, produces whole
419/// `samples_per_ofdm_symbol()`-sized IQ chunks; a partial trailing chunk is
420/// a no-op, with no cross-call buffering. All intermediate buffers are
421/// struct fields sized once in `new()`.
422pub struct OfdmMod {
423 bits_per_symbol: usize,
424 samples_per_symbol: usize,
425 gain: f32,
426 rf_hz: f32,
427 mapper: MapperKind,
428 grid_map: GridMap,
429 ifft: IfftBlock,
430 cp_insert: CyclicPrefixInsert,
431 rot: Rotator,
432 // scratch, sized once in new()
433 syms_scratch: Vec<C32>,
434 freq_scratch: Vec<C32>,
435 time_scratch: Vec<C32>,
436 cp_scratch: Vec<C32>,
437}
438
439impl OfdmMod {
440 pub fn new(cfg: &OfdmConfig) -> Self {
441 let grid = CarrierGrid::from_plan(&cfg.carrier_plan);
442 let n_fft = cfg.carrier_plan.n_fft();
443 let cp_len = cfg.carrier_plan.cp_len();
444 let num_data = grid.num_data_carriers();
445
446 Self {
447 bits_per_symbol: cfg.bits_per_ofdm_symbol(),
448 samples_per_symbol: cfg.samples_per_ofdm_symbol(),
449 gain: cfg.gain,
450 rf_hz: cfg.rf_hz,
451 mapper: MapperKind::new(cfg.constellation),
452 grid_map: GridMap::new(grid),
453 ifft: IfftBlock::new(n_fft),
454 cp_insert: CyclicPrefixInsert::new(n_fft, cp_len),
455 rot: Rotator::new(cfg.rf_hz, cfg.fs),
456 syms_scratch: vec![C32::default(); num_data],
457 freq_scratch: vec![C32::default(); n_fft],
458 time_scratch: vec![C32::default(); n_fft],
459 cp_scratch: vec![C32::default(); n_fft + cp_len],
460 }
461 }
462
463 pub fn set_gain(&mut self, g: f32) {
464 self.gain = g;
465 }
466
467 /// Convenience wrapper mirroring `Ft8Mod::modulate()`: modulates all of
468 /// `bits`, zero-padding a final partial symbol.
469 pub fn modulate(&mut self, bits: &[u8]) -> Vec<C32> {
470 let bps = self.bits_per_symbol;
471 if bps == 0 {
472 return Vec::new();
473 }
474 let n_symbols = bits.len().div_ceil(bps);
475 let mut padded = bits.to_vec();
476 padded.resize(n_symbols * bps, 0);
477
478 let mut out = vec![C32::default(); n_symbols * self.samples_per_symbol];
479 let mut bits_read = 0usize;
480 let mut samples_written = 0usize;
481 while bits_read < padded.len() {
482 let wr = self.process(
483 &padded[bits_read..],
484 &mut out[samples_written..samples_written + self.samples_per_symbol],
485 );
486 if wr.in_read == 0 {
487 break;
488 }
489 bits_read += wr.in_read;
490 samples_written += wr.out_written;
491 }
492 out
493 }
494}
495
496impl Block for OfdmMod {
497 type In = u8;
498 type Out = C32;
499
500 fn process(&mut self, input: &[u8], output: &mut [C32]) -> WorkReport {
501 if input.len() < self.bits_per_symbol || output.len() < self.samples_per_symbol {
502 return WorkReport::default();
503 }
504
505 let map_wr = self
506 .mapper
507 .process(&input[..self.bits_per_symbol], &mut self.syms_scratch);
508 let grid_wr = self
509 .grid_map
510 .process(&self.syms_scratch, &mut self.freq_scratch);
511 let ifft_wr = self
512 .ifft
513 .process(&self.freq_scratch, &mut self.time_scratch);
514 let cp_wr = self
515 .cp_insert
516 .process(&self.time_scratch, &mut self.cp_scratch);
517
518 debug_assert_eq!(map_wr.in_read, self.bits_per_symbol);
519 debug_assert_eq!(grid_wr.out_written, self.ifft.n_fft());
520 debug_assert_eq!(ifft_wr.out_written, self.ifft.n_fft());
521 debug_assert_eq!(cp_wr.out_written, self.samples_per_symbol);
522
523 let g = self.gain;
524 let n = self.samples_per_symbol;
525 if self.rf_hz != 0.0 {
526 for (out, &s) in output[..n].iter_mut().zip(self.cp_scratch[..n].iter()) {
527 let r = self.rot.next();
528 *out = C32::new(
529 g * s.re.mul_add(r.re, -s.im * r.im),
530 g * s.im.mul_add(r.re, s.re * r.im),
531 );
532 }
533 } else {
534 for (out, &s) in output[..n].iter_mut().zip(self.cp_scratch[..n].iter()) {
535 *out = C32::new(g * s.re, g * s.im);
536 }
537 }
538
539 WorkReport {
540 in_read: self.bits_per_symbol,
541 out_written: n,
542 }
543 }
544}