1use std::collections::HashMap;
5use std::collections::hash_map::Entry;
6
7use super::DetectorErrorModel;
8use super::decoder::{ShotDecoder, decode_batch, logical_error_rate};
9use super::dem::symptom_label;
10use crate::error::{PrismError, Result};
11use crate::sim::compiled::PackedShots;
12
13const MAX_EXHAUSTIVE_ORDER: usize = 24;
15const TANH_CLAMP: f64 = 1.0 - 1e-15;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum BpMethod {
21 ProductSum,
23 MinSum { scaling: f64 },
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum OsdMethod {
38 Zero,
40 CombinationSweep { order: usize },
43 Exhaustive { order: usize },
47}
48
49#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct BpOsdOptions {
52 pub max_iterations: usize,
55 pub bp_method: BpMethod,
56 pub osd_method: OsdMethod,
57}
58
59impl Default for BpOsdOptions {
60 fn default() -> Self {
62 Self {
63 max_iterations: 30,
64 bp_method: BpMethod::MinSum { scaling: 0.625 },
65 osd_method: OsdMethod::CombinationSweep { order: 7 },
66 }
67 }
68}
69
70#[derive(Debug, Clone)]
82pub struct BpOsdDecoder {
83 num_detectors: usize,
84 num_observables: usize,
85 obs_words: usize,
86 num_columns: usize,
87 rank: usize,
88 check_offsets: Vec<u32>,
89 slot_column: Vec<u32>,
90 column_offsets: Vec<u32>,
91 column_slots: Vec<u32>,
92 slot_check: Vec<u32>,
93 prior: Vec<f64>,
94 column_obs: Vec<u64>,
95 nonnegative_priors: bool,
96 max_check_degree: usize,
97 options: BpOsdOptions,
98}
99
100impl BpOsdDecoder {
101 pub fn from_model(model: &DetectorErrorModel) -> Result<Self> {
107 Self::with_options(model, BpOsdOptions::default())
108 }
109
110 pub fn with_options(model: &DetectorErrorModel, options: BpOsdOptions) -> Result<Self> {
118 if let BpMethod::MinSum { scaling } = options.bp_method
119 && !(scaling > 0.0 && scaling <= 1.0)
120 {
121 return Err(PrismError::InvalidParameter {
122 message: format!("min-sum scaling {scaling} lies outside (0, 1]"),
123 });
124 }
125 if let OsdMethod::Exhaustive { order } = options.osd_method
126 && order > MAX_EXHAUSTIVE_ORDER
127 {
128 return Err(PrismError::InvalidParameter {
129 message: format!(
130 "exhaustive OSD order {order} exceeds {MAX_EXHAUSTIVE_ORDER}; use the \
131 combination sweep for larger searches"
132 ),
133 });
134 }
135
136 let mut columns: Vec<(&[usize], f64, &[usize])> = Vec::new();
137 let mut slots: HashMap<&[usize], usize> = HashMap::new();
138 for mechanism in model.mechanisms() {
139 let p = mechanism.probability();
140 if !(0.0..1.0).contains(&p) {
141 return Err(PrismError::InvalidParameter {
142 message: format!(
143 "mechanism `{}` has probability {p}, outside [0, 1)",
144 symptom_label(mechanism)
145 ),
146 });
147 }
148 if p == 0.0 || mechanism.detectors().is_empty() {
149 continue;
150 }
151 match slots.entry(mechanism.detectors()) {
152 Entry::Occupied(slot) => {
153 let column = &mut columns[*slot.get()];
154 if p > column.1 {
155 column.1 = p;
156 column.2 = mechanism.observables();
157 }
158 }
159 Entry::Vacant(slot) => {
160 slot.insert(columns.len());
161 columns.push((mechanism.detectors(), p, mechanism.observables()));
162 }
163 }
164 }
165 Ok(Self::from_columns(
166 model.num_detectors(),
167 model.num_observables(),
168 &columns,
169 options,
170 ))
171 }
172
173 fn from_columns(
176 num_detectors: usize,
177 num_observables: usize,
178 columns: &[(&[usize], f64, &[usize])],
179 options: BpOsdOptions,
180 ) -> Self {
181 let obs_words = num_observables.div_ceil(64);
182 let num_columns = columns.len();
183 let mut check_offsets = vec![0u32; num_detectors + 1];
184 for (detectors, ..) in columns {
185 for &d in *detectors {
186 check_offsets[d + 1] += 1;
187 }
188 }
189 for d in 0..num_detectors {
190 check_offsets[d + 1] += check_offsets[d];
191 }
192 let nnz = check_offsets[num_detectors] as usize;
193 let mut cursor = check_offsets.clone();
194 let mut slot_column = vec![0u32; nnz];
195 let mut slot_check = vec![0u32; nnz];
196 let mut column_offsets = vec![0u32; num_columns + 1];
197 let mut column_slots = Vec::with_capacity(nnz);
198 let mut prior = Vec::with_capacity(num_columns);
199 let mut column_obs = vec![0u64; num_columns * obs_words];
200 for (column, (detectors, p, observables)) in columns.iter().enumerate() {
201 for &d in *detectors {
202 let slot = cursor[d];
203 cursor[d] += 1;
204 slot_column[slot as usize] = column as u32;
205 slot_check[slot as usize] = d as u32;
206 column_slots.push(slot);
207 }
208 column_offsets[column + 1] = column_slots.len() as u32;
209 prior.push(((1.0 - p) / p).ln());
210 for &o in *observables {
211 column_obs[column * obs_words + o / 64] |= 1u64 << (o % 64);
212 }
213 }
214 let max_check_degree = (0..num_detectors)
215 .map(|d| (check_offsets[d + 1] - check_offsets[d]) as usize)
216 .max()
217 .unwrap_or(0);
218 let nonnegative_priors = prior.iter().all(|&llr| llr >= 0.0);
219
220 let mut decoder = Self {
221 num_detectors,
222 num_observables,
223 obs_words,
224 num_columns,
225 rank: 0,
226 check_offsets,
227 slot_column,
228 column_offsets,
229 column_slots,
230 slot_check,
231 prior,
232 column_obs,
233 nonnegative_priors,
234 max_check_degree,
235 options,
236 };
237 let mut scratch = decoder.scratch();
238 for (position, column) in scratch.order.iter_mut().enumerate() {
239 *column = position as u32;
240 }
241 scratch.use_all_rows();
242 decoder.rank = decoder.eliminate(&mut scratch, num_columns, num_detectors);
243 decoder
244 }
245
246 pub fn num_detectors(&self) -> usize {
247 self.num_detectors
248 }
249
250 pub fn num_observables(&self) -> usize {
251 self.num_observables
252 }
253
254 pub fn options(&self) -> BpOsdOptions {
255 self.options
256 }
257
258 pub fn decode_packed(&self, detectors: &PackedShots) -> Result<PackedShots> {
268 decode_batch(self, detectors)
269 }
270
271 pub fn logical_error_rate(
279 &self,
280 detectors: &PackedShots,
281 observables: &PackedShots,
282 ) -> Result<f64> {
283 logical_error_rate(&self.decode_packed(detectors)?, observables)
284 }
285
286 fn solve_shot(
289 &self,
290 row: &[u64],
291 out_row: &mut [u64],
292 s: &mut BpOsdScratch,
293 ) -> std::result::Result<f64, Unexplained> {
294 let mut any = false;
295 for (d, bit) in s.syndrome.iter_mut().enumerate() {
296 *bit = (row[d / 64] >> (d % 64)) as u8 & 1;
297 any |= *bit != 0;
298 }
299 s.solution.fill(0);
300 if !any && self.nonnegative_priors {
301 return Ok(0.0);
302 }
303
304 if self.belief_propagation(s) {
305 self.refine_support(s);
306 } else {
307 for (position, column) in s.order.iter_mut().enumerate() {
308 *column = position as u32;
309 }
310 let posterior = &s.posterior;
311 s.order.sort_unstable_by(|&a, &b| {
312 posterior[a as usize]
313 .total_cmp(&posterior[b as usize])
314 .then(a.cmp(&b))
315 });
316 s.use_all_rows();
317 self.ordered_statistics(s, self.num_columns, self.rank)?;
318 }
319 let mut weight = 0.0;
320 for column in 0..self.num_columns {
321 if s.solution[column] != 0 {
322 weight += self.prior[column];
323 let base = column * self.obs_words;
324 for (word, mask) in out_row
325 .iter_mut()
326 .zip(&self.column_obs[base..base + self.obs_words])
327 {
328 *word ^= mask;
329 }
330 }
331 }
332 Ok(weight)
333 }
334
335 fn belief_propagation(&self, s: &mut BpOsdScratch) -> bool {
339 s.posterior.copy_from_slice(&self.prior);
340 for (column, &llr) in self.prior.iter().enumerate() {
341 s.solution[column] = u8::from(llr < 0.0);
342 }
343 if self.satisfies_syndrome(s) {
344 return true;
345 }
346 for (slot, &column) in self.slot_column.iter().enumerate() {
347 s.v2c[slot] = self.prior[column as usize];
348 }
349 for _ in 0..self.options.max_iterations {
350 for check in 0..self.num_detectors {
351 let begin = self.check_offsets[check] as usize;
352 let end = self.check_offsets[check + 1] as usize;
353 let flip = s.syndrome[check] != 0;
354 match self.options.bp_method {
355 BpMethod::MinSum { scaling } => {
356 let mut negative = flip;
357 let mut min1 = f64::INFINITY;
358 let mut min2 = f64::INFINITY;
359 let mut arg = begin;
360 for slot in begin..end {
361 let q = s.v2c[slot];
362 negative ^= q < 0.0;
363 let magnitude = q.abs();
364 if magnitude < min1 {
365 min2 = min1;
366 min1 = magnitude;
367 arg = slot;
368 } else if magnitude < min2 {
369 min2 = magnitude;
370 }
371 }
372 for slot in begin..end {
373 let magnitude = if slot == arg { min2 } else { min1 };
374 let sign_negative = negative ^ (s.v2c[slot] < 0.0);
375 s.c2v[slot] = if sign_negative {
376 -scaling * magnitude
377 } else {
378 scaling * magnitude
379 };
380 }
381 }
382 BpMethod::ProductSum => {
383 let degree = end - begin;
384 let mut forward = 1.0;
385 for at in 0..degree {
386 s.partial[at] = forward;
387 forward *= (0.5 * s.v2c[begin + at]).tanh();
388 }
389 let mut backward = 1.0;
390 for at in (0..degree).rev() {
391 let product = (s.partial[at] * backward).clamp(-TANH_CLAMP, TANH_CLAMP);
392 let message = 2.0 * product.atanh();
393 s.c2v[begin + at] = if flip { -message } else { message };
394 backward *= (0.5 * s.v2c[begin + at]).tanh();
395 }
396 }
397 }
398 }
399 for column in 0..self.num_columns {
400 let begin = self.column_offsets[column] as usize;
401 let end = self.column_offsets[column + 1] as usize;
402 let mut total = self.prior[column];
403 for &slot in &self.column_slots[begin..end] {
404 total += s.c2v[slot as usize];
405 }
406 s.posterior[column] = total;
407 s.solution[column] = u8::from(total < 0.0);
408 for &slot in &self.column_slots[begin..end] {
409 s.v2c[slot as usize] = total - s.c2v[slot as usize];
410 }
411 }
412 if self.satisfies_syndrome(s) {
413 return true;
414 }
415 }
416 false
417 }
418
419 fn satisfies_syndrome(&self, s: &BpOsdScratch) -> bool {
420 (0..self.num_detectors).all(|check| {
421 let begin = self.check_offsets[check] as usize;
422 let end = self.check_offsets[check + 1] as usize;
423 let parity = self.slot_column[begin..end]
424 .iter()
425 .fold(0u8, |acc, &column| acc ^ s.solution[column as usize]);
426 parity == s.syndrome[check]
427 })
428 }
429
430 fn refine_support(&self, s: &mut BpOsdScratch) {
435 let mut count = 0;
436 for column in 0..self.num_columns {
437 if s.solution[column] != 0 {
438 s.order[count] = column as u32;
439 count += 1;
440 }
441 }
442 if count < 2 {
443 return;
444 }
445 let posterior = &s.posterior;
446 s.order[..count].sort_unstable_by(|&a, &b| {
447 posterior[a as usize]
448 .total_cmp(&posterior[b as usize])
449 .then(a.cmp(&b))
450 });
451 s.row_stamp += 1;
452 s.rows.clear();
453 for &column in &s.order[..count] {
454 let begin = self.column_offsets[column as usize] as usize;
455 let end = self.column_offsets[column as usize + 1] as usize;
456 for &slot in &self.column_slots[begin..end] {
457 let check = self.slot_check[slot as usize] as usize;
458 if s.row_mark[check] != s.row_stamp {
459 s.row_mark[check] = s.row_stamp;
460 s.row_local[check] = s.rows.len() as u32;
461 s.rows.push(check as u32);
462 }
463 }
464 }
465 let support_weight = |s: &BpOsdScratch| -> f64 {
466 s.order[..count]
467 .iter()
468 .filter(|&&column| s.solution[column as usize] != 0)
469 .map(|&column| self.prior[column as usize])
470 .sum()
471 };
472 let decision_weight = support_weight(s);
473 let max_rank = count.min(s.rows.len());
474 let refined = self.ordered_statistics(s, count, max_rank);
475 debug_assert!(refined.is_ok(), "the BP decision explains the syndrome");
476 if support_weight(s) > decision_weight {
477 for &column in &s.order[..count] {
478 s.solution[column as usize] = 1;
479 }
480 }
481 }
482
483 fn eliminate(&self, s: &mut BpOsdScratch, count: usize, max_rank: usize) -> usize {
489 let words = count.div_ceil(64).max(1);
490 s.stride = words;
491 let rows = s.rows.len();
492 s.matrix[..rows * words].fill(0);
493 for (position, &column) in s.order[..count].iter().enumerate() {
494 let begin = self.column_offsets[column as usize] as usize;
495 let end = self.column_offsets[column as usize + 1] as usize;
496 for &slot in &self.column_slots[begin..end] {
497 let local = s.row_local[self.slot_check[slot as usize] as usize] as usize;
498 s.matrix[local * words + position / 64] |= 1u64 << (position % 64);
499 }
500 }
501 for (local, &check) in s.rows.iter().enumerate() {
502 s.rhs[local] = s.syndrome[check as usize];
503 }
504
505 let mut rank = 0;
506 for position in 0..count {
507 if rank == max_rank || rank == rows {
508 break;
509 }
510 let word = position / 64;
511 let bit = 1u64 << (position % 64);
512 let Some(pivot) = (rank..rows).find(|&r| s.matrix[r * words + word] & bit != 0) else {
513 continue;
514 };
515 if pivot != rank {
516 for w in 0..words {
517 s.matrix.swap(pivot * words + w, rank * words + w);
518 }
519 s.rhs.swap(pivot, rank);
520 }
521 let (head, tail) = s.matrix.split_at_mut(rank * words);
522 let (pivot_row, tail) = tail.split_at_mut(words);
523 for (r, target) in head.chunks_exact_mut(words).enumerate() {
524 if target[word] & bit != 0 {
525 for (t, p) in target.iter_mut().zip(pivot_row.iter()) {
526 *t ^= p;
527 }
528 s.rhs[r] ^= s.rhs[rank];
529 }
530 }
531 for (offset, target) in tail[..(rows - rank - 1) * words]
532 .chunks_exact_mut(words)
533 .enumerate()
534 {
535 if target[word] & bit != 0 {
536 for (t, p) in target.iter_mut().zip(pivot_row.iter()) {
537 *t ^= p;
538 }
539 s.rhs[rank + 1 + offset] ^= s.rhs[rank];
540 }
541 }
542 s.pivot_position[rank] = position as u32;
543 rank += 1;
544 }
545 rank
546 }
547
548 fn ordered_statistics(
552 &self,
553 s: &mut BpOsdScratch,
554 count: usize,
555 max_rank: usize,
556 ) -> std::result::Result<(), Unexplained> {
557 let rank = self.eliminate(s, count, max_rank);
558 if s.rhs[rank..s.rows.len()].iter().any(|&bit| bit != 0) {
559 return Err(Unexplained);
560 }
561
562 let words = s.stride;
563 let rank_words = rank.div_ceil(64);
564 s.is_pivot[..count].fill(false);
565 for &position in &s.pivot_position[..rank] {
566 s.is_pivot[position as usize] = true;
567 }
568 s.free.clear();
569 for position in 0..count {
570 if !s.is_pivot[position] {
571 s.free.push(position as u32);
572 }
573 }
574 let candidates = match self.options.osd_method {
575 OsdMethod::Zero => 0,
576 OsdMethod::CombinationSweep { .. } => s.free.len(),
577 OsdMethod::Exhaustive { order } => order.min(s.free.len()),
578 };
579
580 s.base[..rank_words].fill(0);
583 for r in 0..rank {
584 s.base[r / 64] |= u64::from(s.rhs[r]) << (r % 64);
585 }
586 s.columns[..candidates * rank_words].fill(0);
587 for (candidate, &position) in s.free[..candidates].iter().enumerate() {
588 let word = position as usize / 64;
589 let shift = position as usize % 64;
590 for r in 0..rank {
591 let bit = (s.matrix[r * words + word] >> shift) & 1;
592 s.columns[candidate * rank_words + r / 64] |= bit << (r % 64);
593 }
594 }
595
596 let column_weight =
597 |s: &BpOsdScratch, position: u32| self.prior[s.order[position as usize] as usize];
598 let pivot_weight = |s: &BpOsdScratch, x: &[u64]| {
599 let mut total = 0.0;
600 for (w, &bits) in x.iter().enumerate() {
601 let mut bits = bits;
602 while bits != 0 {
603 let r = w * 64 + bits.trailing_zeros() as usize;
604 bits &= bits - 1;
605 total += self.prior[s.order[s.pivot_position[r] as usize] as usize];
606 }
607 }
608 total
609 };
610
611 s.trial[..rank_words].copy_from_slice(&s.base[..rank_words]);
612 let mut best_weight = pivot_weight(s, &s.trial[..rank_words]);
613 let mut best = Flips::None;
614 match self.options.osd_method {
615 OsdMethod::Zero => {}
616 OsdMethod::CombinationSweep { order } => {
617 for f in 0..candidates {
618 for w in 0..rank_words {
619 s.trial[w] = s.base[w] ^ s.columns[f * rank_words + w];
620 }
621 let weight =
622 column_weight(s, s.free[f]) + pivot_weight(s, &s.trial[..rank_words]);
623 if weight < best_weight {
624 best_weight = weight;
625 best = Flips::One(f);
626 }
627 }
628 let pairs = order.min(candidates);
629 for f1 in 0..pairs {
630 for f2 in f1 + 1..pairs {
631 for w in 0..rank_words {
632 s.trial[w] = s.base[w]
633 ^ s.columns[f1 * rank_words + w]
634 ^ s.columns[f2 * rank_words + w];
635 }
636 let weight = column_weight(s, s.free[f1])
637 + column_weight(s, s.free[f2])
638 + pivot_weight(s, &s.trial[..rank_words]);
639 if weight < best_weight {
640 best_weight = weight;
641 best = Flips::Two(f1, f2);
642 }
643 }
644 }
645 }
646 OsdMethod::Exhaustive { .. } => {
647 let mut pattern = 0u32;
649 for step in 1..1u32 << candidates {
650 let f = step.trailing_zeros() as usize;
651 pattern ^= 1 << f;
652 for w in 0..rank_words {
653 s.trial[w] ^= s.columns[f * rank_words + w];
654 }
655 let mut free_weight = 0.0;
656 let mut bits = pattern;
657 while bits != 0 {
658 free_weight += column_weight(s, s.free[bits.trailing_zeros() as usize]);
659 bits &= bits - 1;
660 }
661 let weight = free_weight + pivot_weight(s, &s.trial[..rank_words]);
662 if weight < best_weight {
663 best_weight = weight;
664 best = Flips::Pattern(pattern);
665 }
666 }
667 }
668 }
669
670 for &column in &s.order[..count] {
671 s.solution[column as usize] = 0;
672 }
673 s.trial[..rank_words].copy_from_slice(&s.base[..rank_words]);
674 let flip = |s: &mut BpOsdScratch, f: usize| {
675 s.solution[s.order[s.free[f] as usize] as usize] = 1;
676 for w in 0..rank_words {
677 s.trial[w] ^= s.columns[f * rank_words + w];
678 }
679 };
680 match best {
681 Flips::None => {}
682 Flips::One(f) => flip(s, f),
683 Flips::Two(f1, f2) => {
684 flip(s, f1);
685 flip(s, f2);
686 }
687 Flips::Pattern(pattern) => {
688 for f in 0..candidates {
689 if pattern >> f & 1 == 1 {
690 flip(s, f);
691 }
692 }
693 }
694 }
695 for r in 0..rank {
696 let column = s.order[s.pivot_position[r] as usize] as usize;
697 s.solution[column] = (s.trial[r / 64] >> (r % 64)) as u8 & 1;
698 }
699 Ok(())
700 }
701}
702
703#[derive(Debug, Clone, Copy)]
705enum Flips {
706 None,
707 One(usize),
708 Two(usize, usize),
709 Pattern(u32),
710}
711
712pub(super) struct Unexplained;
714
715impl ShotDecoder for BpOsdDecoder {
716 type Scratch = BpOsdScratch;
717 type Failure = Unexplained;
718 #[cfg(feature = "parallel")]
719 const PARALLEL_SHOT_THRESHOLD: usize = 64;
720 #[cfg(feature = "parallel")]
721 const SHOT_CHUNK: usize = 16;
722
723 fn num_detectors(&self) -> usize {
724 self.num_detectors
725 }
726
727 fn num_observables(&self) -> usize {
728 self.num_observables
729 }
730
731 fn scratch(&self) -> BpOsdScratch {
732 BpOsdScratch::new(self)
733 }
734
735 fn decode_shot(
736 &self,
737 row: &[u64],
738 out_row: &mut [u64],
739 scratch: &mut BpOsdScratch,
740 ) -> std::result::Result<(), Unexplained> {
741 self.solve_shot(row, out_row, scratch).map(|_| ())
742 }
743
744 fn failure_error(_: Unexplained, shot: usize) -> PrismError {
745 PrismError::InvalidParameter {
746 message: format!(
747 "shot {shot}: the syndrome lies outside the column space of the model's \
748 check matrix, so it is impossible under the model"
749 ),
750 }
751 }
752}
753
754pub(super) struct BpOsdScratch {
756 syndrome: Vec<u8>,
757 v2c: Vec<f64>,
758 c2v: Vec<f64>,
759 partial: Vec<f64>,
760 posterior: Vec<f64>,
761 solution: Vec<u8>,
762 order: Vec<u32>,
763 rows: Vec<u32>,
764 row_local: Vec<u32>,
765 row_mark: Vec<u64>,
766 row_stamp: u64,
767 stride: usize,
768 matrix: Vec<u64>,
769 rhs: Vec<u8>,
770 pivot_position: Vec<u32>,
771 is_pivot: Vec<bool>,
772 free: Vec<u32>,
773 base: Vec<u64>,
774 trial: Vec<u64>,
775 columns: Vec<u64>,
776}
777
778impl BpOsdScratch {
779 fn use_all_rows(&mut self) {
780 self.rows.clear();
781 for (check, local) in self.row_local.iter_mut().enumerate() {
782 *local = check as u32;
783 self.rows.push(check as u32);
784 }
785 }
786
787 fn new(decoder: &BpOsdDecoder) -> Self {
788 let rows = decoder.num_detectors;
789 let columns = decoder.num_columns;
790 let nnz = decoder.slot_column.len();
791 let row_words = columns.div_ceil(64);
792 let rank_words = rows.min(columns).div_ceil(64);
793 Self {
794 syndrome: vec![0; rows],
795 v2c: vec![0.0; nnz],
796 c2v: vec![0.0; nnz],
797 partial: vec![0.0; decoder.max_check_degree],
798 posterior: vec![0.0; columns],
799 solution: vec![0; columns],
800 order: vec![0; columns],
801 rows: Vec::with_capacity(rows),
802 row_local: vec![0; rows],
803 row_mark: vec![0; rows],
804 row_stamp: 0,
805 stride: row_words,
806 matrix: vec![0; rows * row_words.max(1)],
807 rhs: vec![0; rows],
808 pivot_position: vec![0; rows.min(columns)],
809 is_pivot: vec![false; columns],
810 free: Vec::with_capacity(columns),
811 base: vec![0; rank_words],
812 trial: vec![0; rank_words],
813 columns: vec![0; columns * rank_words],
814 }
815 }
816}
817
818#[cfg(test)]
819#[path = "bposd_tests.rs"]
820mod tests;