1use std::cmp::Ordering;
7use std::collections::{HashMap, HashSet};
8use std::fs::File;
9use std::io::{BufRead, BufReader, BufWriter, Write};
10
11use anyhow::Result;
12
13use crate::annotation::Annotation;
14use crate::is_bsj_hg2::SmithWaterman;
15use crate::utils::{bsj_is_summary_priority, bsj_payload_start};
16
17pub struct Summary {
23 pub stringency: i32,
24 pub circ_count: usize,
26 pub final_bsj_reads: usize,
28}
29
30struct CircSortItem {
35 start_site: i32,
36 line: String,
37}
38
39impl Summary {
40 fn compare_chr_names(a: &str, b: &str) -> Ordering {
49 fn chr_rank(chr: &str) -> (u8, u8, i32, &str) {
50 if let Some(rest) = chr.strip_prefix("chr") {
51 if let Ok(n) = rest.parse::<i32>() {
52 return (0, 0, n, "");
53 }
54 return match rest {
55 "X" => (0, 1, 23, ""),
56 "Y" => (0, 1, 24, ""),
57 "M" | "MT" => (0, 1, 25, ""),
58 _ => (0, 2, 0, rest),
59 };
60 }
61 (1, 0, 0, chr)
62 }
63
64 let ka = chr_rank(a);
65 let kb = chr_rank(b);
66 ka.0.cmp(&kb.0)
67 .then_with(|| ka.1.cmp(&kb.1))
68 .then_with(|| ka.2.cmp(&kb.2))
69 .then_with(|| ka.3.cmp(kb.3))
70 }
71
72 fn annotate_circ(annotation: &Annotation, chr: &str, start: i32, end: i32) -> (String, String) {
80 if let (Some(v1), Some(v2)) = (
81 annotation
82 .chr_exon_start_map
83 .get(&format!("{}\t{}", chr, start)),
84 annotation
85 .chr_exon_end_map
86 .get(&format!("{}\t{}", chr, end)),
87 ) {
88 if let (Some((g1, _)), Some((g2, _))) = (v1.split_once('\t'), v2.split_once('\t')) {
89 if g1 == g2 {
90 return ("exon".to_string(), g1.to_string());
91 }
92 }
93 }
94
95 if let Some(genes) = annotation.chr_gene_map.get(chr) {
96 for gene in genes {
97 if start < gene.start {
98 break;
99 }
100 if end > gene.end {
101 continue;
102 }
103 if let Some(exons) = annotation.gene_exon_map.get(&gene.gene_id) {
104 let start_in_exon = exons
105 .iter()
106 .any(|&(exon_start, exon_end)| exon_start <= start && exon_end >= start);
107 let end_in_exon = exons
108 .iter()
109 .any(|&(exon_start, exon_end)| exon_start <= end && exon_end >= end);
110 if start_in_exon && end_in_exon {
111 return ("exon".to_string(), gene.gene_id.clone());
112 }
113 return ("intron".to_string(), gene.gene_id.clone());
114 }
115 }
116 }
117
118 ("intergenic_region".to_string(), "NA".to_string())
119 }
120
121 #[inline]
122 fn java_string_hash(s: &str) -> i32 {
124 let mut h: i32 = 0;
125 for ch in s.chars() {
126 h = h.wrapping_mul(31).wrapping_add(ch as i32);
127 }
128 h
129 }
130
131 #[inline]
132 fn java_hash_spread(h: i32) -> u32 {
134 let x = h as u32;
135 x ^ (x >> 16)
136 }
137
138 #[inline]
139 fn java_hashset_capacity(n: usize) -> usize {
141 let mut cap = 16usize;
142 while n > ((cap as f64) * 0.75f64) as usize {
143 cap = cap.saturating_mul(2);
144 }
145 cap.max(1)
146 }
147
148 pub fn new(stringency: i32) -> Self {
150 Self {
151 stringency,
152 circ_count: 0,
153 final_bsj_reads: 0,
154 }
155 }
156
157 fn cigar_len_between_ms(cigar: &str) -> i32 {
160 let ops: Vec<char> = cigar.chars().filter(|c| c.is_ascii_alphabetic()).collect();
161 let nums: Vec<i32> = {
162 let mut out = Vec::new();
163 let mut num = 0i32;
164 let mut seen_digit = false;
165 for ch in cigar.chars() {
166 if ch.is_ascii_digit() {
167 num = num * 10 + (ch as i32 - '0' as i32);
168 seen_digit = true;
169 } else if ch.is_ascii_alphabetic() {
170 if seen_digit {
171 out.push(num);
172 }
173 num = 0;
174 seen_digit = false;
175 }
176 }
177 out
178 };
179 if ops.is_empty() || nums.is_empty() || ops.len() != nums.len() {
180 return 0;
181 }
182 let first_m = ops.iter().position(|&c| c == 'M');
183 let last_m = ops.iter().rposition(|&c| c == 'M');
184 if first_m.is_none() || last_m.is_none() {
185 return 0;
186 }
187 let first = first_m.unwrap();
188 let last = last_m.unwrap();
189 let mut sum = 0i32;
190 for i in first..=last {
191 if ops[i] == 'M' || ops[i] == 'D' {
192 sum += nums[i];
193 }
194 }
195 sum
196 }
197
198 fn update_cigar_counts(cigar: &str, counts: &mut [i32; 3]) {
201 let t: String = cigar.chars().filter(|x| x.is_ascii_alphabetic()).collect();
202 if t == "M" {
203 return;
204 } else if t.find('M') > Some(0) && t.rfind('M') < Some(t.len() - 1) {
205 counts[2] += 1;
206 } else if t.find('M') == Some(0) {
207 counts[1] += 1;
208 } else {
209 counts[0] += 1;
210 }
211 }
212
213 fn merge_same_start(
220 circ_map: &mut HashMap<String, HashSet<String>>,
221 chr_tcga_map: &HashMap<String, String>,
222 circ_start_insertion: &HashMap<String, Vec<String>>,
223 ) {
224 let mut aligner = SmithWaterman::new(1, -1, -3);
225 for (chr_start, insertion_list) in circ_start_insertion {
226 if insertion_list.len() <= 1 {
227 continue;
228 }
229 let cap = Self::java_hashset_capacity(insertion_list.len());
232 let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); cap];
233 for (idx, s) in insertion_list.iter().enumerate() {
234 let spread = Self::java_hash_spread(Self::java_string_hash(s));
235 let bucket_idx = (spread as usize) & (cap - 1);
236 buckets[bucket_idx].push(idx);
237 }
238 let mut circ_names: Vec<String> = Vec::with_capacity(insertion_list.len());
239 for bucket in buckets {
240 for idx in bucket {
241 let k = &insertion_list[idx];
242 if circ_map.contains_key(k) {
243 circ_names.push(k.clone());
244 }
245 }
246 }
247 if circ_names.len() <= 1 {
248 continue;
249 }
250 let chr = chr_start.split('\t').next().unwrap_or_default();
251 let chr_seq = match chr_tcga_map.get(chr) {
252 Some(v) => v,
253 None => continue,
254 };
255 let mut score = vec![0i32; circ_names.len()];
256 let mut end_site = vec![0i32; circ_names.len()];
257 let mut cigars = vec![0i32; circ_names.len()];
258
259 for (idx, name) in circ_names.iter().enumerate() {
260 if let Some(lines) = circ_map.get(name) {
261 let mut len = 0i32;
262 for line in lines {
263 let p: Vec<&str> = line.split('\t').collect();
264 if p.len() < 10 || !p[1].contains(';') {
265 continue;
266 }
267 end_site[idx] = p[5].parse().unwrap_or(0);
268 if p[2] == "1" && p[9] == "1" {
269 score[idx] += 1;
270 }
271 let c_arr: Vec<&str> = p[1].split(';').collect();
272 if c_arr.len() >= 2 {
273 let l = Self::cigar_len_between_ms(c_arr[1]);
274 if l > len {
275 len = l;
276 }
277 }
278 }
279 cigars[idx] = len;
280 }
281 }
282
283 let up: Vec<usize> = score
284 .iter()
285 .enumerate()
286 .filter_map(|(i, &v)| if v > 0 { Some(i) } else { None })
287 .collect();
288 let mut zero: Vec<usize> = score
289 .iter()
290 .enumerate()
291 .filter_map(|(i, &v)| if v == 0 { Some(i) } else { None })
292 .collect();
293
294 for up_idx in up {
295 let mut remove_list = Vec::new();
296 for &z_idx in &zero {
297 let len2 = cigars[z_idx];
298 if len2 <= 0 {
299 continue;
300 }
301 let e1 = end_site[up_idx];
302 let e2 = end_site[z_idx];
303 if e1 <= len2 || e2 <= len2 {
304 continue;
305 }
306 let s1 = (e1 - len2) as usize;
307 let s2 = (e2 - len2) as usize;
308 let e1u = e1 as usize;
309 let e2u = e2 as usize;
310 if e1u > chr_seq.len() || e2u > chr_seq.len() || s1 >= e1u || s2 >= e2u {
311 continue;
312 }
313 let seq1 = &chr_seq[s1..e1u];
314 let seq2 = &chr_seq[s2..e2u];
315 aligner.set_seq(seq1, seq2);
316 aligner.align();
317 let pass_score =
318 aligner.score >= seq1.len() as i32 - 2 - ((seq1.len() as i32 - 1) / 10) * 2;
319 let pass_len = aligner.aligned_len > seq1.len() as i32 - 2;
320 if pass_score && pass_len {
321 let up_name = &circ_names[up_idx];
322 let z_name = &circ_names[z_idx];
323 if let (Some(set_up), Some(set_z)) = (
324 circ_map.get(up_name).cloned(),
325 circ_map.get(z_name).cloned(),
326 ) {
327 let mut merged = set_up;
328 merged.extend(set_z);
329 circ_map.insert(up_name.clone(), merged);
330 circ_map.remove(z_name);
331 remove_list.push(z_idx);
332 }
333 }
334 }
335 zero.retain(|v| !remove_list.contains(v));
336 }
337 }
338 }
339
340 fn merge_same_end(
343 circ_map: &mut HashMap<String, HashSet<String>>,
344 chr_tcga_map: &HashMap<String, String>,
345 circ_insertion: &[String],
346 circ_map_capacity: usize,
347 ) {
348 let mut circ_end_seen: HashMap<String, HashSet<String>> = HashMap::new();
349 let mut circ_end_insertion: HashMap<String, Vec<String>> = HashMap::new();
350 let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); circ_map_capacity.max(1)];
351 for (idx, key) in circ_insertion.iter().enumerate() {
352 let spread = Self::java_hash_spread(Self::java_string_hash(key));
353 let bucket_idx = (spread as usize) & (circ_map_capacity.max(1) - 1);
354 buckets[bucket_idx].push(idx);
355 }
356 for bucket in buckets {
357 for idx in bucket {
358 let k = &circ_insertion[idx];
359 if !circ_map.contains_key(k) {
360 continue;
361 }
362 let p: Vec<&str> = k.split('\t').collect();
363 if p.len() != 3 {
364 continue;
365 }
366 let key = format!("{}\t{}", p[0], p[2]);
367 let seen = circ_end_seen.entry(key.clone()).or_default();
368 if seen.insert(k.clone()) {
369 circ_end_insertion.entry(key).or_default().push(k.clone());
370 }
371 }
372 }
373
374 let mut aligner = SmithWaterman::new(1, -1, -3);
375 for (chr_end, insertion_list) in circ_end_insertion {
376 if insertion_list.len() <= 1 {
377 continue;
378 }
379 let cap = Self::java_hashset_capacity(insertion_list.len());
384 let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); cap];
385 for (idx, s) in insertion_list.iter().enumerate() {
386 let spread = Self::java_hash_spread(Self::java_string_hash(s));
387 let bucket_idx = (spread as usize) & (cap - 1);
388 buckets[bucket_idx].push(idx);
389 }
390 let circ_names: Vec<String> = buckets
391 .into_iter()
392 .flat_map(|bucket| bucket.into_iter())
393 .map(|idx| insertion_list[idx].clone())
394 .filter(|k| circ_map.contains_key(k))
395 .collect();
396 if circ_names.len() <= 1 {
397 continue;
398 }
399 let chr = chr_end.split('\t').next().unwrap_or_default();
400 let chr_seq = match chr_tcga_map.get(chr) {
401 Some(v) => v,
402 None => continue,
403 };
404 let mut score = vec![0i32; circ_names.len()];
405 let mut start_site = vec![0i32; circ_names.len()];
406 let mut cigars = vec![0i32; circ_names.len()];
407
408 for (idx, name) in circ_names.iter().enumerate() {
409 if let Some(lines) = circ_map.get(name) {
410 let mut len = 0i32;
411 for line in lines {
412 let p: Vec<&str> = line.split('\t').collect();
413 if p.len() < 10 || !p[1].contains(';') {
414 continue;
415 }
416 start_site[idx] = p[4].parse().unwrap_or(0);
417 if p[2] == "1" && p[9] == "1" {
418 score[idx] += 1;
419 }
420 let c_arr: Vec<&str> = p[1].split(';').collect();
421 if !c_arr.is_empty() {
422 let l = Self::cigar_len_between_ms(c_arr[0]);
423 if l > len {
424 len = l;
425 }
426 }
427 }
428 cigars[idx] = len;
429 }
430 }
431
432 let up: Vec<usize> = score
433 .iter()
434 .enumerate()
435 .filter_map(|(i, &v)| if v > 0 { Some(i) } else { None })
436 .collect();
437 let mut zero: Vec<usize> = score
438 .iter()
439 .enumerate()
440 .filter_map(|(i, &v)| if v == 0 { Some(i) } else { None })
441 .collect();
442
443 for up_idx in up {
444 let mut remove_list = Vec::new();
445 for &z_idx in &zero {
446 let len2 = cigars[z_idx];
447 if len2 <= 0 {
448 continue;
449 }
450 let s1 = start_site[up_idx];
451 let s2 = start_site[z_idx];
452 if s1 < 0 || s2 < 0 {
453 continue;
454 }
455 let e1 = s1 + len2;
456 let e2 = s2 + len2;
457 if e1 as usize > chr_seq.len() || e2 as usize > chr_seq.len() {
458 continue;
459 }
460 let seq1 = &chr_seq[s1 as usize..e1 as usize];
461 let seq2 = &chr_seq[s2 as usize..e2 as usize];
462 aligner.set_seq(seq1, seq2);
463 aligner.align();
464 let pass_score =
465 aligner.score >= seq1.len() as i32 - 2 - ((seq1.len() as i32 - 1) / 10) * 2;
466 let pass_len = aligner.aligned_len > seq1.len() as i32 - 2;
467 if pass_score && pass_len {
468 let up_name = &circ_names[up_idx];
469 let z_name = &circ_names[z_idx];
470 if let (Some(set_up), Some(set_z)) = (
471 circ_map.get(up_name).cloned(),
472 circ_map.get(z_name).cloned(),
473 ) {
474 let mut merged = set_up;
475 merged.extend(set_z);
476 circ_map.insert(up_name.clone(), merged);
477 circ_map.remove(z_name);
478 remove_list.push(z_idx);
479 }
480 }
481 }
482 zero.retain(|v| !remove_list.contains(v));
483 }
484 }
485 }
486
487 pub fn run(
493 &mut self,
494 bsj1_file: &str,
495 result_path: &str,
496 fsj_map: &HashMap<String, i32>,
497 chr_tcga_map: &HashMap<String, String>,
498 annotation: &Annotation,
499 ) -> Result<()> {
500 self.run_from_bsj_files(&[bsj1_file], result_path, fsj_map, chr_tcga_map, annotation)
501 }
502
503 pub fn run_from_bsj_files(
509 &mut self,
510 bsj_files: &[&str],
511 result_path: &str,
512 fsj_map: &HashMap<String, i32>,
513 chr_tcga_map: &HashMap<String, String>,
514 annotation: &Annotation,
515 ) -> Result<()> {
516 self.circ_count = 0;
517 self.final_bsj_reads = 0;
518 let mut circ_map: HashMap<String, HashSet<String>> = HashMap::new();
519 let mut circ_start_seen: HashMap<String, HashSet<String>> = HashMap::new();
520 let mut circ_start_insertion: HashMap<String, Vec<String>> = HashMap::new();
521 let mut circ_seen: HashSet<String> = HashSet::new();
522 let mut circ_insertion: Vec<String> = Vec::new();
523 for bsj_file in bsj_files {
524 let file = File::open(bsj_file)?;
525 let reader = BufReader::new(file);
526 for line_res in reader.lines() {
527 let line = line_res?;
528 let p: Vec<&str> = line.split('\t').collect();
529 if p.len() < 8 || !bsj_is_summary_priority(&p) {
530 continue;
531 }
532 let payload_start = bsj_payload_start(&p);
533 if p.len() <= payload_start + 8 {
534 continue;
535 }
536 let legacy_line = if payload_start == 1 {
537 line.clone()
538 } else {
539 format!("{}\t{}", p[0], p[payload_start..].join("\t"))
540 };
541 let legacy: Vec<&str> = legacy_line.split('\t').collect();
542 let key = format!("{}\t{}\t{}", legacy[3], legacy[4], legacy[5]);
543 circ_map
544 .entry(key.clone())
545 .or_default()
546 .insert(legacy_line.clone());
547 if circ_seen.insert(key.clone()) {
548 circ_insertion.push(key.clone());
549 }
550 let start_key = format!("{}\t{}", legacy[3], legacy[4]);
551 let seen = circ_start_seen.entry(start_key.clone()).or_default();
552 if seen.insert(key.clone()) {
553 circ_start_insertion.entry(start_key).or_default().push(key);
554 }
555 }
556 }
557
558 Self::merge_same_start(&mut circ_map, chr_tcga_map, &circ_start_insertion);
559 let circ_map_capacity = Self::java_hashset_capacity(circ_insertion.len());
560 Self::merge_same_end(
561 &mut circ_map,
562 chr_tcga_map,
563 &circ_insertion,
564 circ_map_capacity,
565 );
566
567 let mut final_results: HashMap<String, Vec<CircSortItem>> = HashMap::new();
568 let mut final_read_ids: HashSet<String> = HashSet::new();
569 for (chr_start_end, lines) in &circ_map {
570 let p_key: Vec<&str> = chr_start_end.split('\t').collect();
571 if p_key.len() != 3 {
572 continue;
573 }
574
575 let mut circ_id_set3: HashSet<String> = HashSet::new();
576 let mut cigar_set3: HashSet<String> = HashSet::new();
577 let mut false_cigar_set3: HashSet<String> = HashSet::new();
578 let mut circ_id_set: HashSet<String> = HashSet::new();
579 let mut cigar_set: HashSet<String> = HashSet::new();
580 let mut false_cigar_set: HashSet<String> = HashSet::new();
581 let mut non_reads = 0i32;
582 let mut fp_reads = 0i32;
583 let mut tag = 0i32;
584 let mut strand = " ".to_string();
585
586 for line in lines {
587 let p: Vec<&str> = line.split('\t').collect();
588 if p.len() < 10 {
589 continue;
590 }
591 strand = p[6].to_string();
592 if p[2] == "1" {
593 if p[9] == "3" {
594 cigar_set3.insert(p[1].to_string());
595 circ_id_set3.insert(p[0].to_string());
596 } else {
597 for c in p[1].split(';') {
598 cigar_set.insert(c.to_string());
599 }
600 if p[9] == "1" {
601 tag += 1;
602 }
603 circ_id_set.insert(p[0].to_string());
604 }
605 } else if p[2] == "-1" {
606 if p[9] == "3" {
607 false_cigar_set3.insert(p[1].to_string());
608 } else {
609 for c in p[1].split(';') {
610 false_cigar_set.insert(c.to_string());
611 }
612 non_reads += 1;
613 }
614 } else if p[2] == "-2" {
615 if p[9] == "3" {
616 false_cigar_set3.insert(p[1].to_string());
617 } else {
618 for c in p[1].split(';') {
619 false_cigar_set.insert(c.to_string());
620 }
621 fp_reads += 1;
622 }
623 }
624 }
625
626 cigar_set3.extend(cigar_set.iter().cloned());
627 circ_id_set3.extend(circ_id_set.iter().cloned());
628 false_cigar_set3.extend(false_cigar_set.iter().cloned());
629
630 let mut cigar_count3 = [0, 0, 0];
631 for c in &cigar_set3 {
632 Self::update_cigar_counts(c, &mut cigar_count3);
633 }
634
635 let tp_reads = circ_id_set.len() as i32;
636 let tp_reads3 = circ_id_set3.len() as i32;
637
638 let passed = if self.stringency == 2 {
639 ((tp_reads > 19 * fp_reads || fp_reads <= 1)
640 && tp_reads > non_reads + fp_reads
641 && cigar_set.len() >= 3
642 && tp_reads >= 2)
643 || (tag > 0
644 && false_cigar_set3.is_empty()
645 && cigar_set3.len() >= 3
646 && tp_reads3 >= 2)
647 } else if self.stringency == 1 {
648 ((tp_reads > 19 * fp_reads || false_cigar_set.len() <= 2)
649 && tp_reads > non_reads + fp_reads
650 && tp_reads >= 2)
651 || (tag > 0 && false_cigar_set3.is_empty() && tp_reads3 >= 2)
652 } else {
653 ((tp_reads > 19 * fp_reads || false_cigar_set.len() <= 2)
654 && tp_reads > non_reads + fp_reads)
655 || (tag > 0 && false_cigar_set3.is_empty() && tp_reads3 >= 2)
656 };
657
658 if !passed {
659 continue;
660 }
661
662 let fsj = *fsj_map.get(chr_start_end).unwrap_or(&0);
663 let ratio = if tp_reads3 * 2 + fsj > 0 {
664 (tp_reads3 * 2) as f64 / (tp_reads3 * 2 + fsj) as f64
665 } else {
666 0.0
667 };
668 let start = p_key[1].parse::<i32>().unwrap_or(0);
669 let (circ_type, gene_id) =
670 Self::annotate_circ(annotation, p_key[0], start, p_key[2].parse().unwrap_or(0));
671 let mut ids: Vec<String> = circ_id_set3.into_iter().collect();
672 ids.sort();
673 for id in &ids {
674 final_read_ids.insert(id.clone());
675 }
676 let line = format!(
677 "{}:{}|{}\t{}\t{}\t{}\t{}\t{}_{}_{}\t{}\t{:.2}\t{}\t{}\t{}\t{}\t{}",
678 p_key[0],
679 p_key[1],
680 p_key[2],
681 p_key[0],
682 p_key[1],
683 p_key[2],
684 tp_reads3,
685 cigar_count3[0],
686 cigar_count3[1],
687 cigar_count3[2],
688 fsj,
689 ratio,
690 circ_type,
691 gene_id,
692 strand,
693 ids.join(","),
694 tag
695 );
696 final_results
697 .entry(p_key[0].to_string())
698 .or_default()
699 .push(CircSortItem {
700 start_site: start,
701 line,
702 });
703 }
704
705 let out_file = File::create(result_path)?;
706 let mut writer = BufWriter::with_capacity(1024 * 1024, out_file);
707 writeln!(writer, "circRNA_ID\tchr\tcircRNA_start\tcircRNA_end\t#junction_reads\tSM_MS_SMS\t#non_junction_reads\tjunction_reads_ratio\tcircRNA_type\tgene_id\tstrand\tjunction_reads_ID\tScore")?;
708 let mut chrs: Vec<String> = final_results.keys().cloned().collect();
709 chrs.sort_by(|a, b| Self::compare_chr_names(a, b));
710 for chr in chrs {
711 let items = final_results.get_mut(&chr).expect("chromosome key exists");
712 items.sort_by_key(|x| x.start_site);
713 for item in items {
714 writeln!(writer, "{}", item.line)?;
715 self.circ_count += 1;
716 }
717 }
718 writer.flush()?;
719 self.final_bsj_reads = final_read_ids.len();
720 Ok(())
721 }
722}
723
724#[cfg(test)]
725mod tests {
726 use super::Summary;
727
728 #[test]
729 fn test_compare_chr_names_main_chromosomes_first() {
730 let mut chrs = vec![
731 "GL000225.1".to_string(),
732 "chr10".to_string(),
733 "chr2".to_string(),
734 "KI270442.1".to_string(),
735 "chrX".to_string(),
736 "chr1".to_string(),
737 "chrM".to_string(),
738 "chrUn_KI270442v1".to_string(),
739 ];
740 chrs.sort_by(|a, b| Summary::compare_chr_names(a, b));
741 assert_eq!(
742 chrs,
743 vec![
744 "chr1",
745 "chr2",
746 "chr10",
747 "chrX",
748 "chrM",
749 "chrUn_KI270442v1",
750 "GL000225.1",
751 "KI270442.1",
752 ]
753 );
754 }
755}