1 /***************************************************************
2
3 The Subread software package is free software package:
4 you can redistribute it and/or modify it under the terms
5 of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License,
7 or (at your option) any later version.
8
9 Subread is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty
11 of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12
13 See the GNU General Public License for more details.
14
15 Authors: Drs Yang Liao and Wei Shi
16
17 ***************************************************************/
18
19
20 #include <assert.h>
21 #include <stdlib.h>
22 #include <string.h>
23 #include "LRMsorted-hashtable.h"
24
25 #if !defined(__APPLE__) && !defined(__FreeBSD__) && !defined(__DragonFly__)
26 #include <malloc.h>
27 #endif
28
29 #include<math.h>
30 #include "LRMfile-io.h"
31
32 #define _gehash_hash(k) ((unsigned int)(k))
33 #define WITHOUT_CLUSTER_ORDERING 0
34
LRM_gehash_get_bucket(LRMgehash_t * the_table,LRMgehash_key_t key)35 struct LRMgehash_bucket * LRM_gehash_get_bucket(LRMgehash_t * the_table, LRMgehash_key_t key) {
36 int bucket_number;
37
38 bucket_number = _gehash_hash(key) % the_table -> buckets_number;
39 return the_table -> buckets +bucket_number;
40 }
41
42
43
44 #define INDEL_SEGMENT_SIZE 5
45
46 #define _index_vote(key) (((unsigned int)(key))%LRMGENE_VOTE_TABLE_SIZE)
47 #define _index_vote_tol(key) (((unsigned int)(key)/INDEL_SEGMENT_SIZE)%LRMGENE_VOTE_TABLE_SIZE)
48
49
50 #define is_quality_subread(scr) ((scr)>15?1:0)
51
52
LRMgehash_go_q(LRMgehash_t * the_table,LRMgehash_key_t raw_key,int offset,int read_len,int is_reversed,LRMgene_vote_t * vote,int indel_tolerance,int subread_number)53 size_t LRMgehash_go_q(LRMgehash_t * the_table, LRMgehash_key_t raw_key, int offset, int read_len, int is_reversed, LRMgene_vote_t * vote, int indel_tolerance, int subread_number){
54 //LRMprintf("Q=%u, OFFSET=%d, B=%u ~ %u\n", raw_key, offset, low_border, high_border);
55
56 // VER_1
57 // VER_2
58
59 struct LRMgehash_bucket * current_bucket;
60 int i = 0, items;
61
62 short *current_keys;//, *endp12;
63 short key = raw_key / the_table->buckets_number;
64
65 current_bucket = LRM_gehash_get_bucket (the_table, raw_key);
66 items = current_bucket -> current_items;
67 current_keys = current_bucket -> new_item_keys;
68
69 if(!items) return 0;
70
71 int imin=0, imax=items;
72 int last_accepted_index = 0;
73
74 while( imin < items )
75 {
76 last_accepted_index=(imin+imax)/2;
77 short current_key = current_keys[last_accepted_index];
78 if(current_key>key)
79 {
80 imax = last_accepted_index - 1;
81 }
82 else if(current_key<key)
83 {
84 imin = last_accepted_index + 1;
85 }
86 else
87 break;
88
89 if(imax<imin)
90 return 0;
91
92 }
93
94 while(last_accepted_index){
95 if(current_keys[last_accepted_index-1] == key) last_accepted_index-=1;
96 else break;
97 }
98
99 int ii_end = INDEL_SEGMENT_SIZE;
100 if(indel_tolerance>5) ii_end=(indel_tolerance % INDEL_SEGMENT_SIZE)?(indel_tolerance - indel_tolerance%INDEL_SEGMENT_SIZE+INDEL_SEGMENT_SIZE):indel_tolerance;
101
102 for (; last_accepted_index<items && current_keys[last_accepted_index] == key ; last_accepted_index++)
103 {
104 unsigned int kv = current_bucket->item_values[last_accepted_index] - offset;
105 int iix, offsetX2, offsetX, datalen, datalen2;
106 offsetX2 = offsetX = _index_vote_tol(kv);
107 datalen = datalen2 = vote -> items[offsetX2];
108 unsigned int * dat2, *dat;
109 dat = dat2 = vote -> pos[offsetX2];
110
111 //LRMprintf("You can find KV at %u\n", kv);
112
113 for(iix = 0; iix<=ii_end; iix = iix>0?-iix:(-iix+INDEL_SEGMENT_SIZE))
114 {
115 if(iix)
116 {
117 offsetX = _index_vote_tol(kv+iix);
118 datalen = vote -> items[offsetX];
119 dat = vote -> pos[offsetX];
120 }
121
122
123 if(!datalen)continue;
124
125 for (i=0;i<datalen;i++)
126 {
127 int di = dat[i];
128 int dist0 = kv-di;
129 if( dist0 >= -indel_tolerance && dist0 <= indel_tolerance )
130 {
131 if(is_reversed == (0!=(vote -> masks[offsetX][i]&LRMIS_NEGATIVE_STRAND)))
132 {
133 if(offset < vote->coverage_end [offsetX][i] + 10){
134 unsigned char test_max = (vote->votes[offsetX][i]);
135 test_max += 1;
136 vote -> votes[offsetX][i] = test_max;
137
138 if (offset +16 > vote->coverage_end [offsetX][i])
139 vote->coverage_end [offsetX][i] = offset+16;
140
141 /*
142 int toli = vote -> toli[offsetX][i];
143
144 if (dist0 != vote->current_indel_cursor[offsetX][i])
145 {
146 toli +=3;
147 if (toli < LRMMAX_INDEL_SECTIONS*3)
148 {
149 vote -> toli[offsetX][i] = toli;
150 vote -> indel_recorder[offsetX][i][toli] = subread_number+1;
151 vote -> indel_recorder[offsetX][i][toli+1] = subread_number+1;
152 vote -> indel_recorder[offsetX][i][toli+2] = dist0;
153 //LRMprintf("subread=#%d ,TOLI=%d, DIST0=%d, POS=%u \n", subread_number, toli, dist0, kv);
154
155 if(toli < LRMMAX_INDEL_SECTIONS*3-3) vote -> indel_recorder[offsetX][i][toli+3]=0;
156 }
157 vote->current_indel_cursor [offsetX][i] = (char)dist0;
158 } else vote -> indel_recorder[offsetX][i][toli+1] = subread_number+1;
159 */
160 i = 9999999;
161 }
162 }
163 }
164 }
165 if (i>=9999999){
166 break;
167 }
168
169 }
170
171 if (i < 9999999)
172 {
173 if (datalen2<LRMGENE_VOTE_SPACE)
174 {
175 vote -> items[offsetX2] ++;
176 dat2[datalen2] = kv;
177 vote -> masks[offsetX2][datalen2]=(is_reversed?LRMIS_NEGATIVE_STRAND:0);
178 vote -> votes[offsetX2][datalen2]=1;
179 vote -> toli[offsetX2][datalen2]=0;
180
181 // data structure of recorder:
182 // {unsigned char subread_start; unsigned char subread_end, char indel_offset_from_start}
183 // All subread numbers are added with 1 for not being 0.
184
185 vote -> indel_recorder[offsetX2][datalen2][0] = vote -> indel_recorder[offsetX2][datalen2][1] = subread_number+1;
186 vote -> indel_recorder[offsetX2][datalen2][2] = 0;
187 vote -> indel_recorder[offsetX2][datalen2][3] = 0;
188 vote -> current_indel_cursor [offsetX2][datalen2] = 0;
189 vote -> coverage_start [offsetX2][datalen2] = offset;
190 vote -> coverage_end [offsetX2][datalen2] = offset+16;
191 //LRMprintf("subread=#%d ,NEW RECORD =%u\n", subread_number, kv);
192 }
193 }
194 else i=0;
195 }
196 return 1;
197 }
198
199
LRMindel_recorder_copy(unsigned short * dst,unsigned short * src)200 short LRMindel_recorder_copy(unsigned short *dst, unsigned short * src)
201 {
202 short all_indels = 0;
203 // memcpy(dst, src, 3*MAX_INDEL_TOLERANCE); return;
204
205
206 int i=0;
207 while(src[i] && (i<3*LRMMAX_INDEL_TOLERANCE-2))
208 {
209 dst[i] = src[i];
210 i++;
211 dst[i] = src[i];
212 i++;
213 dst[i] = src[i];
214 all_indels = dst[i];
215 i++;
216 }
217 dst[i] = 0;
218 return all_indels;
219
220 }
221
222 // Data Struct of dumpping:
223 // {
224 // size_t current_items;
225 // size_t buckets_number;
226 // struct
227 // {
228 // size_t current_items;
229 // size_t space_size;
230 // gehash_key_t item_keys [current_items];
231 // gehash_data_t item_values [current_items]
232 // } [buckets_number];
233 // }
234 //
235
LRMload_int32(FILE * fp)236 unsigned int LRMload_int32(FILE * fp)
237 {
238 int ret;
239 int read_length;
240 read_length = fread(&ret, sizeof(int), 1, fp);
241 if(read_length<=0)assert(0);
242 return ret;
243 }
244
LRMload_int64(FILE * fp)245 long long int LRMload_int64(FILE * fp)
246 {
247 long long int ret;
248 int read_length;
249 read_length = fread(&ret, sizeof(long long int), 1, fp);
250 if(read_length<=0)assert(0);
251 return ret;
252 }
253
254
LRMgehash_load_option(const char fname[],int option_no,int * result)255 int LRMgehash_load_option(const char fname [], int option_no, int * result){
256 char tabname[LRMMAX_FILENAME_LENGTH];
257 char magic_chars[8];
258 int found = 0;
259 sprintf(tabname, "%s.00.b.tab", fname);
260 FILE * fp = fopen(tabname, "rb");
261 if(fp == NULL){
262 sprintf(tabname, "%s.00.c.tab", fname);
263 fp = fopen(tabname, "rb");
264 }
265 if(fp){
266 int rlen = fread(magic_chars,1,8,fp);
267 if(rlen < 8){
268 LRMprintf("ERROR: Unable to read-in the index.\n");
269 return -1;
270 }
271 if(memcmp(magic_chars, "2subindx",7)==0) {
272 while(1) {
273 short option_key, option_length;
274
275 rlen = fread(&option_key, 2, 1, fp);
276 if(rlen<1){
277 LRMprintf("ERROR: Unable to read-in the index.\n");
278 return -1;
279 }
280
281 if(!option_key) break;
282
283 rlen = fread(&option_length, 2, 1, fp);
284 if(rlen<1){
285 LRMprintf("ERROR: Unable to read-in the index.\n");
286 return -1;
287 }
288
289
290 if(option_key == option_no){
291 *result = 0;
292 rlen = fread(result ,option_length,1,fp);
293 if(rlen<1){
294 LRMprintf("ERROR: Unable to read-in the index.\n");
295 return -1;
296 }
297 found = 1;
298 }
299 else
300 fseek(fp, option_length, SEEK_CUR);
301 }
302 }
303 fclose(fp);
304 }
305 return found;
306 }
307
LRMgehash_load(LRMgehash_t * the_table,const char fname[])308 int LRMgehash_load(LRMgehash_t * the_table, const char fname [])
309 {
310 int i, read_length;
311 char magic_chars[8];
312 magic_chars[7]=0;
313
314 the_table -> index_gap = 0;
315
316 FILE * fp = fopen(fname, "rb");
317 if (!fp)
318 {
319 LRMprintf ("Table file '%s' is not found.\n", fname);
320 return 1;
321 }
322
323 int rlen = fread(magic_chars,1,8,fp);
324 if(rlen<8){
325 LRMprintf("ERROR: Unable to read-in the index.\n");
326 return -1;
327 }
328
329 if(memcmp(magic_chars+1, "subindx",7)==0)
330 {
331 if('2'==magic_chars[0])
332 the_table -> version_number = LRMSUBINDEX_VER2;
333 else assert(0);
334
335 while(1)
336 {
337 short option_key, option_length;
338
339 rlen = fread(&option_key, 2, 1, fp);
340 if(rlen<1){
341 LRMprintf("ERROR: Unable to read-in the index.\n");
342 return -1;
343 }
344 if(!option_key) break;
345
346 rlen = fread(&option_length, 2, 1, fp);
347 if(rlen<1){
348 LRMprintf("ERROR: Unable to read-in the index.\n");
349 return -1;
350 }
351
352 rlen = 999;
353 if(option_key == LRMSUBREAD_INDEX_OPTION_INDEX_GAP)
354 rlen = fread(&(the_table -> index_gap),2,1,fp);
355 else if (option_key == LRMSUBREAD_INDEX_OPTION_INDEX_PADDING)
356 rlen = fread(&(the_table -> padding),2,1,fp);
357 else
358 fseek(fp, option_length, SEEK_CUR);
359
360 if(rlen<1){
361 LRMprintf("ERROR: Unable to read-in the index.\n");
362 return -1;
363 }
364 }
365 assert(the_table -> index_gap);
366
367 the_table -> current_items = LRMload_int64(fp);
368 if(the_table -> current_items < 1 || the_table -> current_items > 0xffffffffllu){
369 LRMputs("ERROR: the index format is unrecognizable.");
370 return 1;
371 }
372 the_table -> buckets_number = LRMload_int32(fp);
373 the_table -> buckets = (struct LRMgehash_bucket * )malloc(sizeof(struct LRMgehash_bucket) * the_table -> buckets_number);
374 if(!the_table -> buckets)
375 {
376 LRMputs("Error: out of memory");
377 return 1;
378 }
379
380 for (i=0; i<the_table -> buckets_number; i++)
381 {
382 struct LRMgehash_bucket * current_bucket = &(the_table -> buckets[i]);
383 current_bucket -> current_items = LRMload_int32(fp);
384 current_bucket -> space_size = LRMload_int32(fp);
385 current_bucket -> space_size = current_bucket -> current_items;
386 current_bucket -> new_item_keys = (short *) malloc ( sizeof(short) * current_bucket -> space_size);
387 current_bucket -> item_values = (LRMgehash_data_t *) malloc ( sizeof(LRMgehash_data_t) * current_bucket -> space_size);
388
389 if(!(current_bucket -> new_item_keys&¤t_bucket -> item_values))
390 {
391 LRMputs("Error: out of memory");
392 return 1;
393
394 }
395
396 if(current_bucket -> current_items > 0)
397 {
398 read_length = fread(current_bucket -> new_item_keys, sizeof(short), current_bucket -> current_items, fp);
399 if(read_length < current_bucket -> current_items){
400 LRMprintf("ERROR: the index is incomplete : %d < %u.\n",read_length, current_bucket -> current_items);
401 return 1;
402 }
403 read_length = fread(current_bucket -> item_values, sizeof(LRMgehash_data_t), current_bucket -> current_items, fp);
404 if(read_length < current_bucket -> current_items){
405 LRMprintf("ERROR: the index value is incomplete : %d < %u.\n",read_length, current_bucket -> current_items);
406 return 1;
407 }
408 }
409
410 }
411
412 read_length = fread(&(the_table -> is_small_table), sizeof(char), 1, fp);
413 assert(read_length>0);
414 fclose(fp);
415 return 0;
416
417 }
418 else assert(0);
419 return 0;
420 }
421
LRMtest2key(unsigned int kk,char * obuf)422 void LRMtest2key(unsigned int kk, char * obuf){
423 int xx,oo=0;
424 for(xx=0; xx<32;xx++){
425 obuf[oo++] = (kk & (1<<xx)) ?'1':'0';
426 if(xx%2 == 1 && xx < 31) obuf[oo++]=' ';
427 }
428 obuf[oo]=0;
429 }
430
LRMtest2key_dist(unsigned int k1,unsigned int k2)431 int LRMtest2key_dist(unsigned int k1, unsigned int k2){
432 int xx, ret = 0;
433 for(xx=0; xx<16;xx++){
434 int b1 = (k1 >> (xx*2)) & 3;
435 int b2 = (k2 >> (xx*2)) & 3;
436 if(b1!=b2) ret++;
437 }
438 return ret;
439 }
440
LRMgehash_go_QQ(LRMcontext_t * context,LRMthread_context_t * thread_context,LRMread_iteration_context_t * iteration_context,LRMgehash_t * the_table,LRMgehash_key_t raw_key,int offset,int read_len,int is_reversed,LRMgene_vote_t * vote,int indel_tolerance,int subread_number)441 size_t LRMgehash_go_QQ(LRMcontext_t * context, LRMthread_context_t * thread_context, LRMread_iteration_context_t * iteration_context, LRMgehash_t * the_table, LRMgehash_key_t raw_key, int offset, int read_len, int is_reversed, LRMgene_vote_t * vote, int indel_tolerance, int subread_number){
442
443 struct LRMgehash_bucket * current_bucket;
444 int i = 0, items;
445
446 short *current_keys;//, *endp12;
447 short key = raw_key / the_table->buckets_number;
448
449 current_bucket = LRM_gehash_get_bucket (the_table, raw_key);
450 items = current_bucket -> current_items;
451 current_keys = current_bucket -> new_item_keys;
452
453 if(!items) return 0;
454
455 int imin=0, imax=items;
456 int last_accepted_index = 0;
457
458 while( imin < items )
459 {
460 last_accepted_index=(imin+imax)/2;
461 short current_key = current_keys[last_accepted_index];
462 if(current_key>key)
463 imax = last_accepted_index - 1;
464 else if(current_key<key)
465 imin = last_accepted_index + 1;
466 else
467 break;
468
469 if(imax<imin)
470 return 0;
471
472 }
473
474 while(last_accepted_index){
475 if(current_keys[last_accepted_index-1] == key) last_accepted_index-=1;
476 else break;
477 }
478
479 for (; last_accepted_index<items && current_keys[last_accepted_index] == key ; last_accepted_index++)
480 {
481 unsigned int kv = current_bucket->item_values[last_accepted_index] - offset;
482 int offsetX, datalen;
483 offsetX = _index_vote(kv);
484 datalen = vote -> items[offsetX];
485 unsigned int *dat;
486 dat = vote -> pos[offsetX];
487
488
489 for (i=0;i<datalen;i++)
490 {
491 int di = dat[i];
492 if( kv == di && is_reversed == (0!=(vote -> masks[offsetX][i]&LRMIS_NEGATIVE_STRAND)) && offset < vote->coverage_end [offsetX][i] + 14){
493 vote -> votes[offsetX][i]++;
494
495 if (offset +16 > vote->coverage_end [offsetX][i])
496 vote->coverage_end [offsetX][i] = offset+16;
497 i = 9999999;
498 }
499 }
500
501 if (i < 9999999)
502 {
503 if (datalen<LRMGENE_VOTE_SPACE)
504 {
505 vote -> items[offsetX] ++;
506 dat[datalen]=kv;
507 vote -> masks[offsetX][datalen]=(is_reversed?LRMIS_NEGATIVE_STRAND:0);
508 vote -> votes[offsetX][datalen]=1;
509 vote -> coverage_start [offsetX][datalen] = offset;
510 vote -> coverage_end [offsetX][datalen] = offset+16;
511 }
512 }
513 else i=0;
514 }
515 return 1;
516 }
517
LRMgehash_go_tolerance(LRMcontext_t * context,LRMthread_context_t * thread_context,LRMread_iteration_context_t * iteration_context,LRMgehash_t * the_table,LRMgehash_key_t key,int offset,int read_len,int is_reversed,LRMgene_vote_t * vote,int indel_tolerance,int subread_number,int max_MM)518 size_t LRMgehash_go_tolerance(LRMcontext_t * context, LRMthread_context_t * thread_context, LRMread_iteration_context_t * iteration_context, LRMgehash_t * the_table, LRMgehash_key_t key, int offset, int read_len, int is_reversed, LRMgene_vote_t * vote, int indel_tolerance, int subread_number, int max_MM){
519 if(max_MM >= 10) return 0;
520 int ret = 0;
521
522 ret+=LRMgehash_go_q(the_table, key, offset, read_len, is_reversed, vote, indel_tolerance, subread_number);
523 int error_bases ;
524 for (error_bases=1; error_bases <= max_MM; error_bases++)
525 {
526 assert(error_bases<5);
527 int i, j;
528 char error_pos_stack[10]; // max error bases = 10;
529 LRMgehash_key_t mutation_key;
530
531 for(i=0; i<error_bases; i++)
532 error_pos_stack [i] = i;
533 while (1)
534 {
535
536 char mutation_stack[10];
537 memset(mutation_stack, 0 , error_bases);
538 while(1)
539 {
540 int base_to_change=-1;
541 mutation_key = key;
542
543 for (j = 0; j < error_bases; j++)
544 {
545 base_to_change = error_pos_stack[j];
546 int old_value = (key >> 2*base_to_change) & 3;
547 int new_index = mutation_stack[j];
548
549 int new_value;
550 if(old_value <= new_index) new_value = 1+ new_index;
551 else new_value = new_index;
552
553 mutation_key = mutation_key & ~(0x3 << (2*base_to_change));
554 mutation_key = mutation_key | (new_value << (2*base_to_change));
555 }
556
557 if(key != mutation_key ){
558 int dret=LRMgehash_go_q(the_table, mutation_key, offset, read_len, is_reversed, vote, indel_tolerance, subread_number);
559 ret += dret;
560 if(0 && LRMFIXLENstrcmp("@39076b1f-df29-4487-be51-4c30bf6c1cc4_Basecall_Alignment_template", iteration_context->read_name)==0){
561 char bin_mutation_key[53], bin_key[53];
562 LRMtest2key(mutation_key, bin_mutation_key);
563 LRMtest2key(key, bin_key);
564 LRMprintf("GO_TOLE_TEST: %s + %d DIST = %d HITS = %d :\nNEWKY: %s\nORGKY: %s\n", iteration_context->read_name , offset, LRMtest2key_dist(mutation_key,key), dret, bin_mutation_key, bin_key);
565 }
566 }
567 // increase one in the mutation_stack
568 mutation_stack[error_bases-1]++;
569 for(i = error_bases-1; i>=0; i--){
570 if( mutation_stack[i]>2 ) {
571 if(i == 0)break;
572 else{
573 mutation_stack[i] = 0;
574 mutation_stack[i-1]++;
575 }
576 }
577 }
578 if(mutation_stack[0]>2)break;
579 }
580
581
582 int is_end = 1;
583 for (i = error_bases-1; i>=0; i--)
584 if (error_pos_stack[i] < 16 - (error_bases - i))
585 {
586 error_pos_stack[i] ++;
587 for (j = i+1; j<error_bases; j++)
588 error_pos_stack[j] = error_pos_stack[i] + (j-i);
589 is_end = 0;
590 break;
591 }
592
593 if(is_end) break;
594 }
595 }
596 return ret;
597 }
598
LRMgehash_destory(LRMgehash_t * the_table)599 void LRMgehash_destory(LRMgehash_t * the_table)
600 {
601 int i;
602
603 for (i=0; i<the_table -> buckets_number; i++)
604 {
605 struct LRMgehash_bucket * current_bucket = &(the_table -> buckets[i]);
606 if (current_bucket -> space_size > 0)
607 {
608 free (current_bucket -> new_item_keys);
609 free (current_bucket -> item_values);
610 }
611 }
612
613 free (the_table -> buckets);
614
615 the_table -> current_items = 0;
616 the_table -> buckets_number = 0;
617 }
618
LRMprint_v(LRMcontext_t * context,LRMread_iteration_context_t * iteration_context,int min_votes)619 void LRMprint_v(LRMcontext_t * context, LRMread_iteration_context_t * iteration_context, int min_votes){
620 LRMgene_vote_t * v = &iteration_context-> vote_table;
621
622 LRMprintf(" ==== VOTING TABLE ========================= \n");
623 int iii, jjj;
624 for(iii=0; iii < LRMGENE_VOTE_TABLE_SIZE; iii++){
625 for(jjj = 0; jjj < v -> items[iii]; jjj++){
626 unsigned int this_pos = v -> pos[iii][jjj];
627 int votes = v -> votes[iii][jjj];
628 if(votes >= min_votes){
629 char postxt[100];
630 int conf_start = v -> coverage_start[iii][jjj] , conf_end = v -> coverage_end [iii][jjj];
631 LRMpos2txt(context, this_pos, postxt);
632 LRMprintf(" %d (%s) %3d - %3d : %16s ", votes,(v -> masks[iii][jjj]&LRMIS_NEGATIVE_STRAND)?"NEG":"POS", conf_start, conf_end, postxt);
633 int ix;
634 for(ix=0; v->indel_recorder[iii][jjj][ix]; ix+=3){
635 LRMprintf(" %d ~ %d : %d ", v->indel_recorder[iii][jjj][ix] , v->indel_recorder[iii][jjj][ix+1], v->indel_recorder[iii][jjj][ix+2]);
636 }
637 LRMprintf("\n");
638 }
639 }
640 }
641 LRMprintf(" =========================================== \n\n");
642 }
643