Mercurial > repos > itaxotools > mold
comparison MolD_v1.4.py @ 0:4e8e2f836d0f draft default tip
planemo upload commit 232ce39054ce38be27c436a4cabec2800e14f988-dirty
| author | itaxotools |
|---|---|
| date | Sun, 29 Jan 2023 16:25:48 +0000 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:4e8e2f836d0f |
|---|---|
| 1 """ | |
| 2 This script compiles rDNC-based DNA diagnoses for a pre-defined taxa in a dataset. This is the MAIN WORKING VERSION v1.4 | |
| 3 This version already implements the new functionalities: | |
| 4 - automatic trimming of the alignment to match the median sequence length (should seq len distribution and provided NumberN settings require that) | |
| 5 - expanded qCLADEs setting | |
| 6 - diagnoses in pairwise comparisons of taxa. | |
| 7 - selection of a reference sequence for indexing DNCs | |
| 8 | |
| 9 THIS version LACKS SPART compatibility | |
| 10 | |
| 11 """ | |
| 12 import os, sys | |
| 13 import random | |
| 14 import argparse | |
| 15 from io import StringIO | |
| 16 ######################################################################## FUNCTIONS | |
| 17 #***STEP 1 - SORTING ENTRIES BY CLADE AND IDENTIFYING NUCLEOTIDE POSITIONS SHARED WITHIN CLADE | |
| 18 def Step1(raw_records): | |
| 19 Clades=[] | |
| 20 for i in range(len(raw_records)): | |
| 21 Clade=raw_records[i][1] | |
| 22 if Clade not in Clades: | |
| 23 Clades.append(Clade) | |
| 24 clade_sorted_seqs = {} | |
| 25 for letter in Clades: | |
| 26 clade_sorted_seqs[letter]=[] | |
| 27 for i in range(len(raw_records)): | |
| 28 if raw_records[i][1]==letter: | |
| 29 clade_sorted_seqs[letter].append(raw_records[i][2]) | |
| 30 shared_positions={} | |
| 31 for key in clade_sorted_seqs: | |
| 32 sh_pos=[] | |
| 33 for i in range(len(clade_sorted_seqs[key][0])): | |
| 34 shared_nucleotide = True | |
| 35 csm = clade_sorted_seqs[key][0][i] #candidate shared nucleotide | |
| 36 for j in range(1, len(clade_sorted_seqs[key])): | |
| 37 if clade_sorted_seqs[key][j][i] != csm: | |
| 38 shared_nucleotide = False | |
| 39 break | |
| 40 if shared_nucleotide == True and csm != 'N': | |
| 41 sh_pos.append(i) | |
| 42 shared_positions[key]=sh_pos | |
| 43 return Clades, clade_sorted_seqs, shared_positions | |
| 44 | |
| 45 #***STEP 2 COMPILING COMPARISON LISTS FOR CLADES AND IDENTIFYING VARIABLE POSITIONS AND N PRIORITY POSITIONS WITH LARGEST CUTOFFS | |
| 46 def C_VP_PP(clade_sorted_seqs, clade, shared_positions, CUTOFF):# complist_variable_positions_priority_positions; Arguments: dictionary, string, dictionary | |
| 47 CShN={}#a dictionary keys - clade shared positions, values - nucleotides at those positions | |
| 48 for pos in shared_positions[clade]: | |
| 49 CShN[pos] = clade_sorted_seqs[clade][0][pos]#creates a dictionary shared position : nucleotide | |
| 50 complist=[] | |
| 51 for key in clade_sorted_seqs: | |
| 52 if key != clade: | |
| 53 complist = complist + clade_sorted_seqs[key]#creates a list of all other sequences for comparison | |
| 54 cutoffs = {} | |
| 55 pures = []####! newline | |
| 56 for key in CShN: | |
| 57 newcomplist = [] | |
| 58 for k in complist: | |
| 59 if k[key] == CShN[key]: | |
| 60 newcomplist.append(k) | |
| 61 else: continue | |
| 62 cutoffs[key] = len(complist) - len(newcomplist) | |
| 63 if len(newcomplist) == 0:####! newline | |
| 64 pures.append(key)####! newline | |
| 65 CPP = [] | |
| 66 for key in sorted(cutoffs, key = cutoffs.get, reverse = True): | |
| 67 CPP.append(key) | |
| 68 if CUTOFF[0] == '>':#VERYNEW | |
| 69 Clade_priority_positions = {pos:CShN[pos] for pos in CPP if cutoffs[pos] > int(CUTOFF[1:])}#VERYNEW | |
| 70 else:#VERYNEW | |
| 71 Clade_priority_positions = {} | |
| 72 for position in CPP[:int(CUTOFF)]:#Here you define how many of the clade shared combinations are used in subsequent search | |
| 73 Clade_priority_positions[position] = CShN[position] | |
| 74 return complist, Clade_priority_positions, cutoffs, pures####! pures added | |
| 75 | |
| 76 #***STEPS 3 RANDOM SEARCH ACROSS PRIORITY POSITIONS TO FIND RAW DIAGNOSTIC COMBINATIONS AND TO SUBSEQUENTLY REFINE THEM | |
| 77 def random_position(somelist, checklist):#gives a random index (integer) of the specified range, and returns indexed somelist element if it is not present in the checklist | |
| 78 while True: | |
| 79 i = random.randint(0, len(somelist) - 1) | |
| 80 if somelist[i] not in checklist: | |
| 81 return somelist[i] | |
| 82 break | |
| 83 else: | |
| 84 continue | |
| 85 | |
| 86 def step_reduction_complist(clade, complist, CPP, checked_ind):#checks randomly selected positions of CladeSharedNucleotides with sequences of other clades, until a diagnostic combination of nucleotides for a selected clade is found. | |
| 87 if len(complist) == 0: | |
| 88 return checked_ind | |
| 89 elif len(checked_ind) == len(CPP): | |
| 90 return checked_ind | |
| 91 else: | |
| 92 newcomplist = [] | |
| 93 pos = random_position(list(CPP.keys()), checked_ind) | |
| 94 for j in complist: | |
| 95 if j[pos] == CPP[pos] or j[pos] == 'N':#VERYNEW | |
| 96 newcomplist.append(j) | |
| 97 else: continue | |
| 98 new_checked_ind = checked_ind + [pos] | |
| 99 return step_reduction_complist(clade, newcomplist, CPP, new_checked_ind) | |
| 100 | |
| 101 def ConditionD(newcomb, complist, CPP):#The function checks the 'Condition D' - i.e. whither any given combination of nucleotide positions is diagnostic for the selected clade | |
| 102 ContD = False | |
| 103 for i in newcomb: | |
| 104 newcomplist = [] | |
| 105 for m in complist: | |
| 106 if m[i] == CPP[i]: | |
| 107 newcomplist.append(m) | |
| 108 else: continue | |
| 109 complist = newcomplist | |
| 110 if len(complist) == 0: | |
| 111 ContD = True | |
| 112 return ContD | |
| 113 | |
| 114 def RemoveRedundantPositions(raw_comb, complist, CPP):# The function removes positions from the raw combinations one by one, and then checks whether new combination fulfills the condition D, thus recursively reducing the diagnostic combination. | |
| 115 red_possible = False | |
| 116 for j in raw_comb: | |
| 117 newcomb = [k for k in raw_comb if k != j] | |
| 118 if ConditionD(newcomb, complist, CPP) == True: | |
| 119 red_possible = True | |
| 120 return RemoveRedundantPositions(newcomb, complist, CPP) | |
| 121 else: pass | |
| 122 if red_possible == False: | |
| 123 return raw_comb | |
| 124 | |
| 125 #PUTS EVERYTHING TOGETHER - 20000 ROUNDS OF RANDOM SEARCH FOLLOWED BY REFINING OF 500 SHORTEST COMBINATIONS | |
| 126 def Diagnostic_combinations(qCLADE, complist, CPP, n1, maxlen1, maxlen2): | |
| 127 Achecked_ind = [] | |
| 128 bestlists = [] | |
| 129 n = n1 | |
| 130 while n>0:#STEP3 proposes raw diagnostic combinations | |
| 131 raw_comb = step_reduction_complist(qCLADE, complist, CPP, Achecked_ind) | |
| 132 if len(raw_comb) <= maxlen1: | |
| 133 refined_comb = RemoveRedundantPositions(raw_comb, complist, CPP) | |
| 134 if len(refined_comb) <= maxlen2 and sorted(refined_comb) not in bestlists: | |
| 135 bestlists.append(sorted(refined_comb)) | |
| 136 n=n-1 | |
| 137 bestlists.sort(key=len) | |
| 138 return bestlists | |
| 139 | |
| 140 #***STEP 4 ANALYSIS OF OUTPUT rDNCs | |
| 141 def IndependentKey(diagnostic_combinations):#PRESENTLY NOT INVOLVED - returns independent diagnostic nucleotide combinations, and identifies key nucleotide positions | |
| 142 independent_combinations = [] | |
| 143 selected_positions = [] | |
| 144 for i in range(len(diagnostic_combinations)): | |
| 145 if len(selected_positions) == 0: | |
| 146 for j in range(0, i): | |
| 147 if len(set(diagnostic_combinations[i]) & set(diagnostic_combinations[j])) == 0 and len(set(diagnostic_combinations[i]) & set(selected_positions)) == 0: | |
| 148 independent_combinations.append(diagnostic_combinations[i]) | |
| 149 independent_combinations.append(diagnostic_combinations[j]) | |
| 150 for k in range(len(diagnostic_combinations[i])): | |
| 151 selected_positions.append(diagnostic_combinations[i][k]) | |
| 152 for l in range(len(diagnostic_combinations[j])): | |
| 153 selected_positions.append(diagnostic_combinations[j][l]) | |
| 154 else: | |
| 155 if len(set(diagnostic_combinations[i]) & set(selected_positions)) == 0: | |
| 156 independent_combinations.append(diagnostic_combinations[i]) | |
| 157 for k in range(len(diagnostic_combinations[i])): | |
| 158 selected_positions.append(diagnostic_combinations[i][k]) | |
| 159 independent_combinations.sort(key=len) | |
| 160 key_positions = [] | |
| 161 for pos in diagnostic_combinations[0]: | |
| 162 KP = True | |
| 163 for combination in diagnostic_combinations[1:]: | |
| 164 if pos not in combination: | |
| 165 KP = False | |
| 166 break | |
| 167 else: continue | |
| 168 if KP == True: | |
| 169 key_positions.append(pos) | |
| 170 return independent_combinations, key_positions | |
| 171 | |
| 172 #SPECIFIC FUNCTIONS FOR THE rDNCs | |
| 173 def PositionArrays(Motifs):#VERYNEW ALL FUNCTION NEW | |
| 174 PositionArrays = [] | |
| 175 VarPosList = [] | |
| 176 for i in range(len(Motifs[0])): | |
| 177 Const = True | |
| 178 array = [Motifs[0][i]] | |
| 179 for j in range(len(Motifs[1:])): | |
| 180 if Motifs[j][i] != 'N': | |
| 181 if Motifs[j][i] != array[-1]: | |
| 182 Const = False | |
| 183 array.append(Motifs[j][i]) | |
| 184 PositionArrays.append(array) | |
| 185 if Const == False: | |
| 186 VarPosList.append(i) | |
| 187 return PositionArrays, VarPosList | |
| 188 | |
| 189 def random_sequence_new(SEQ, PositionArrays, VarPosList, Pdiff):#VERYNEW FUNCTION REVISED | |
| 190 #print(["ROUND", len(SEQ)*Pdiff/100, round(len(SEQ)*Pdiff/100)]) | |
| 191 n = round(len(SEQ)*Pdiff/100) | |
| 192 N = random.sample(list(range(1, n)), 1)[0] | |
| 193 PosToChange = random.sample([p for p in VarPosList if SEQ[p] != 'D'], N)#this is a new definition to keep alignment gaps unchanged | |
| 194 NEWSEQ = '' | |
| 195 for i in range(len(SEQ)): | |
| 196 if i not in PosToChange: | |
| 197 NEWSEQ = NEWSEQ + SEQ[i] | |
| 198 else: | |
| 199 newarray = [j for j in PositionArrays[i] if j != SEQ[i]] | |
| 200 newbase = random.sample(newarray, 1)[0] | |
| 201 NEWSEQ = NEWSEQ + newbase | |
| 202 return NEWSEQ | |
| 203 | |
| 204 def GenerateBarcode_new(Diagnostic_combinations, length):#VERYNEW FUNCTION REVISED - This function calculates diagnostic combinations and assembles a barcode of desired length for a query taxon. First all single position DNCs are added, then based on the frequency of a nucleotide position in the DNCs of the 2 positions, and then based on the frequency of a position in longer DNCs | |
| 205 len1 = [] | |
| 206 len2 = [] | |
| 207 lenmore = [] | |
| 208 for comb in Diagnostic_combinations: | |
| 209 if len(comb) == len(Diagnostic_combinations[0]): | |
| 210 for i in comb: | |
| 211 len1.append(i) | |
| 212 elif len(comb) == len(Diagnostic_combinations[0])+1: | |
| 213 for j in comb: | |
| 214 len2.append(j) | |
| 215 else: | |
| 216 for k in comb: | |
| 217 lenmore.append(k) | |
| 218 if len(Diagnostic_combinations[0]) == 1: | |
| 219 Setin = len1 | |
| 220 else: | |
| 221 Setin = [] | |
| 222 for pos in sorted(len1, key=len1.count, reverse = True): | |
| 223 if not pos in Setin: | |
| 224 Setin.append(pos) | |
| 225 for pos1 in sorted(len2, key=len2.count, reverse = True): | |
| 226 if not pos1 in Setin: | |
| 227 Setin.append(pos1) | |
| 228 for pos2 in sorted(lenmore, key=lenmore.count, reverse = True): | |
| 229 if not pos2 in Setin: | |
| 230 Setin.append(pos2) | |
| 231 return Setin[:length] | |
| 232 | |
| 233 def Screwed_dataset_new(raw_records, nseq_per_clade_to_screw, PositionArrays, VarPosList, Percent_difference, Taxon, Cutoff):#VERYNEW FUNCTION REVISED | |
| 234 clades=[] | |
| 235 for i in range(len(raw_records)): | |
| 236 Clade=raw_records[i][1] | |
| 237 if Clade not in clades: | |
| 238 clades.append(Clade) | |
| 239 clade_sorted_seqs = {} | |
| 240 for letter in clades: | |
| 241 clade_sorted_seqs[letter]=[] | |
| 242 for i in range(len(raw_records)): | |
| 243 if raw_records[i][1]==letter: | |
| 244 clade_sorted_seqs[letter].append(raw_records[i][2]) | |
| 245 | |
| 246 for clade in clades: | |
| 247 seqlist = clade_sorted_seqs[clade] | |
| 248 newseqs = [] | |
| 249 if len(seqlist) > nseq_per_clade_to_screw: | |
| 250 iSTS = random.sample(list(range(len(seqlist))), nseq_per_clade_to_screw) | |
| 251 for k in range(len(seqlist)): | |
| 252 if k in iSTS: | |
| 253 newseq = random_sequence_new(seqlist[k], PositionArrays, VarPosList, Percent_difference) | |
| 254 else: | |
| 255 newseq = seqlist[k] | |
| 256 newseqs.append(newseq) | |
| 257 elif len(clade_sorted_seqs[clade]) == nseq_per_clade_to_screw: | |
| 258 for k in range(len(seqlist)): | |
| 259 newseq = random_sequence_new(seqlist[k], PositionArrays, VarPosList, Percent_difference) | |
| 260 newseqs.append(newseq) | |
| 261 else: | |
| 262 for i in range(nseq_per_clade_to_screw): | |
| 263 seq = random.sample(seqlist, 1)[0] | |
| 264 newseq = random_sequence_new(seq, PositionArrays, VarPosList, Percent_difference) | |
| 265 newseqs.append(newseq) | |
| 266 clade_sorted_seqs[clade] = newseqs | |
| 267 | |
| 268 shared_positions={} | |
| 269 for key in clade_sorted_seqs: | |
| 270 sh_pos=[] | |
| 271 for i in range(len(clade_sorted_seqs[key][0])): | |
| 272 shared_nucleotide = True | |
| 273 csm = clade_sorted_seqs[key][0][i] #candidate shared nucleotide | |
| 274 for j in range(1, len(clade_sorted_seqs[key])): | |
| 275 if clade_sorted_seqs[key][j][i] != csm: | |
| 276 shared_nucleotide = False | |
| 277 break | |
| 278 if shared_nucleotide == True and csm != 'N': | |
| 279 sh_pos.append(i) | |
| 280 shared_positions[key]=sh_pos | |
| 281 | |
| 282 x,y,z,pures = C_VP_PP(clade_sorted_seqs, Taxon, shared_positions, Cutoff)#STEP2#### | |
| 283 return x, y | |
| 284 | |
| 285 #NEWFUNCYIONS | |
| 286 def medianSeqLen(listofseqs):#OCT2022 | |
| 287 seqlens = [i.count('A')+i.count('C')+i.count('G')+i.count('T') for i in listofseqs] | |
| 288 medlen = sorted(seqlens)[int(len(seqlens)/2)] | |
| 289 medseq = listofseqs[seqlens.index(medlen)] | |
| 290 start = min([medseq.find('A'),medseq.find('C'),medseq.find('G'),medseq.count('T')]) | |
| 291 if not 'N' in medseq[start:]: | |
| 292 end = len(medseq) | |
| 293 else: | |
| 294 for i in range(start, len(medseq), 1): | |
| 295 if medseq[i:].count('N') == len(medseq[i:]): | |
| 296 end = i | |
| 297 break | |
| 298 return medlen, start, end | |
| 299 | |
| 300 def getAllPairs(taxalist): | |
| 301 uniquetaxapairs = [] | |
| 302 stl = sorted(list(set(taxalist))) | |
| 303 for i in range(len(stl)): | |
| 304 for j in range(i+1, len(stl)): | |
| 305 uniquetaxapairs.append([stl[i],stl[j]]) | |
| 306 return uniquetaxapairs | |
| 307 | |
| 308 | |
| 309 ################################################READ IN PARAMETER FILE AND DATA FILE | |
| 310 | |
| 311 def get_args(): #arguments needed to give to this script | |
| 312 parser = argparse.ArgumentParser(description="run MolD") | |
| 313 required = parser.add_argument_group("required arguments") | |
| 314 required.add_argument("-i", help="textfile with parameters of the analysis", required=True) | |
| 315 return parser.parse_args() | |
| 316 | |
| 317 def mainprocessing(gapsaschars=None, taxalist=None, taxonrank=None, cutoff=None, numnucl=None, numiter=None, maxlenraw=None, maxlenrefined=None, iref=None, pdiff=None, nmax=None, thresh=None, tmpfname=None, origfname=None): | |
| 318 ParDict = {} | |
| 319 if not(all([gapsaschars, taxalist, taxonrank, cutoff, numnucl, numiter, maxlenraw, maxlenrefined, iref, pdiff, nmax, thresh, tmpfname])): | |
| 320 args = get_args() | |
| 321 with open(args.i) as params: | |
| 322 for line in params: | |
| 323 line = line.strip() | |
| 324 if line.startswith('#'): | |
| 325 pass | |
| 326 else: | |
| 327 if len(line.split('=')) == 2 and len(line.split('=')[1]) != 0: | |
| 328 ParDict[line.split('=')[0]] = line.split('=')[1].replace(' ', '')#VERYNEW | |
| 329 else: | |
| 330 ParDict['Gaps_as_chars'] = gapsaschars | |
| 331 ParDict['qTAXA'] = taxalist | |
| 332 ParDict['Taxon_rank'] = taxonrank | |
| 333 ParDict['INPUT_FILE'] = tmpfname | |
| 334 ParDict['ORIG_FNAME'] = origfname# I do not understand this ORIG_FNAME, it throws an error with the command line | |
| 335 ParDict['Cutoff'] = cutoff | |
| 336 ParDict['NumberN'] = numnucl | |
| 337 ParDict['Number_of_iterations'] = numiter | |
| 338 ParDict['MaxLen1'] = maxlenraw | |
| 339 ParDict['MaxLen2'] = maxlenrefined | |
| 340 ParDict['Iref'] = iref | |
| 341 ParDict['Pdiff'] = pdiff | |
| 342 #ParDict['PrSeq'] = prseq | |
| 343 ParDict['NMaxSeq'] = nmax | |
| 344 ParDict['Scoring'] = thresh | |
| 345 ParDict['OUTPUT_FILE'] = "str" | |
| 346 print(ParDict) | |
| 347 ############################################# #VERYNEW HOW GAPS ARE TREATED | |
| 348 #REQUIRES A NEW FIELD IN THE GUI | |
| 349 if ParDict['Gaps_as_chars'] == 'yes': | |
| 350 gaps2D = True#VERYNEW | |
| 351 else:#VERYNEW | |
| 352 gaps2D = False#VERYNEW | |
| 353 | |
| 354 ############################################ DATA FILE | |
| 355 checkread = open(ParDict['INPUT_FILE'], 'r') | |
| 356 firstline = checkread.readline() | |
| 357 checkread.close() | |
| 358 imported=[]#set up a new list with species and identifiers | |
| 359 if '>' in firstline: | |
| 360 f = open(ParDict['INPUT_FILE'], 'r') | |
| 361 for line in f:#VERYNEW - THE DATA READING FROM ALIGNMENT FILE IS ALL REVISED UNTIL f.close() | |
| 362 line=line.rstrip() | |
| 363 if line.startswith('>'): | |
| 364 data = line[1:].split('|') | |
| 365 if len(data) != 2: | |
| 366 print('Check number of entries in', data[0]) | |
| 367 #break | |
| 368 data.append('') | |
| 369 imported.append(data) | |
| 370 else: | |
| 371 if gaps2D == True:# | |
| 372 DNA = line.upper().replace('-', 'D').replace('R', 'N').replace('Y', 'N').replace('S','N').replace('W','N').replace('M','N').replace('K','N')#VERYNEW | |
| 373 else: | |
| 374 DNA = line.upper().replace('-', 'N').replace('R', 'N').replace('Y', 'N').replace('S','N').replace('W','N').replace('M','N').replace('K','N')#VERYNEW | |
| 375 imported[-1][-1] = imported[-1][-1]+DNA | |
| 376 f.close() | |
| 377 else: | |
| 378 f = open(ParDict['INPUT_FILE'], 'r') | |
| 379 for line in f:#VERYNEW - THE DATA READING FROM ALIGNMENT FILE IS ALL REVISED UNTIL f.close() | |
| 380 line=line.rstrip() | |
| 381 data = line.split('\t') | |
| 382 if len(data) != 3: | |
| 383 print('Check number of entries in', data[0]) | |
| 384 ID = data[0] | |
| 385 thetax = data[1] | |
| 386 if gaps2D == True:# | |
| 387 DNA = data[2].upper().replace('-', 'D').replace('R', 'N').replace('Y', 'N').replace('S','N').replace('W','N').replace('M','N').replace('K','N')#VERYNEW | |
| 388 else: | |
| 389 DNA = data[2].upper().replace('-', 'N').replace('R', 'N').replace('Y', 'N').replace('S','N').replace('W','N').replace('M','N').replace('K','N')#VERYNEW | |
| 390 imported.append([ID, thetax, DNA]) | |
| 391 f.close() | |
| 392 | |
| 393 if 'NumberN' in list(ParDict.keys()):#How many ambiguously called nucleotides are allowed | |
| 394 NumberN = int(ParDict['NumberN']) | |
| 395 else: | |
| 396 NumberN = 5 | |
| 397 | |
| 398 if len(set([len(i[2]) for i in imported])) != 1: | |
| 399 print('Alignment contains sequences of different lengths:', set([len(i[2]) for i in imported])) | |
| 400 else: | |
| 401 mlen, sstart, send = medianSeqLen([i[2] for i in imported])#OCT2022 - start | |
| 402 if mlen + NumberN < len(imported[0][2]): | |
| 403 Slice = True | |
| 404 FragmentLen = mlen | |
| 405 corr = sstart | |
| 406 else: | |
| 407 Slice = False | |
| 408 FragmentLen = len(imported[0][2]) | |
| 409 sstart = 0 | |
| 410 send = FragmentLen+1 | |
| 411 corr = 0 | |
| 412 raw_records=[] | |
| 413 for i in imported: | |
| 414 if ParDict['Iref'] != 'NO' and ParDict['Iref'].split(',')[0] == i[0] and ParDict['Iref'].split(',')[1] in ['ex', 'excl', 'out']: | |
| 415 continue | |
| 416 else: | |
| 417 if i[2][sstart:send].count('N') < NumberN and len(i[2][sstart:send]) == FragmentLen: | |
| 418 newi = [i[0], i[1], i[2][sstart:send]] | |
| 419 raw_records.append(newi)#OCT2022 - end | |
| 420 print('\n########################## PARAMETERS ######################\n')#VERYNEW | |
| 421 #print('input file:', ParDict['ORIG_FNAME']) #Outcommented ORIG_FNAME | |
| 422 print('input file:', ParDict['INPUT_FILE']) #Replacement of the line above | |
| 423 print('Coding gaps as characters:', gaps2D) | |
| 424 print('Maximum undetermined nucleotides allowed:', NumberN) | |
| 425 print('Length of the alignment:', len(imported[0][2]),'->', FragmentLen) | |
| 426 print('Indexing reference:', ParDict['Iref'].replace('NO', 'Not set').replace('in', 'included').replace('ex', 'excluded')) | |
| 427 print('Read in', len(raw_records), 'sequences') | |
| 428 | |
| 429 PosArrays, VarPosList = PositionArrays([i[2] for i in raw_records])#VERYNEW | |
| 430 | |
| 431 #############################################READ IN OTHER ANALYSIS PARAMETERS | |
| 432 ##OCT2022 - start | |
| 433 withplus = [] | |
| 434 P2 = [] | |
| 435 shift = True | |
| 436 if ParDict['qTAXA'][0] == '>':#THIS OPTION DIAGNOSES ALL TAXA WITH MORE THAN USER-DEFINED NUMBER OF SEQUENCES AVAILABLE | |
| 437 NumSeq = int(ParDict['qTAXA'][1:]) | |
| 438 Taxarecords = [i[1] for i in raw_records] | |
| 439 qCLADEs = [] | |
| 440 for j in set(Taxarecords): | |
| 441 if Taxarecords.count(j) >= NumSeq: | |
| 442 qCLADEs.append(j) | |
| 443 elif ParDict['qTAXA'].startswith('P:'):#THIS OPTION DIAGNOSES ALL TAXA CONTAINING A USER-DEFINED PATTERN IN THE NAME | |
| 444 pattern = ParDict['qTAXA'].split(':')[1] | |
| 445 Taxarecords = [i[1] for i in raw_records] | |
| 446 qCLADEs = [] | |
| 447 for j in set(Taxarecords): | |
| 448 if pattern in j: | |
| 449 qCLADEs.append(j) | |
| 450 elif ParDict['qTAXA'].startswith('P+:'):#THIS OPTION POOLS ALL TAXA CONTAINING A USER-DEFINED PATTERN IN THE NAME IN ONE TAXON AND DIAGNOSES IT | |
| 451 pattern = ParDict['qTAXA'].split(':')[1] | |
| 452 Taxarecords = set([i[1] for i in raw_records if pattern in i[1]]) | |
| 453 spp = '+'.join(sorted(list(Taxarecords))) | |
| 454 qCLADEs = [spp] | |
| 455 nrecords = [] | |
| 456 for rec in raw_records: | |
| 457 if rec[1] in Taxarecords: | |
| 458 nrecords.append([rec[0], spp, rec[2]]) | |
| 459 else: | |
| 460 nrecords.append(rec) | |
| 461 raw_records = nrecords | |
| 462 else:#THIS OPTION DIAGNOSES ALL TAXA FROM A USER-DEFINED LIST; TAXA MAY BE COMBINED BY USING '+' | |
| 463 qCLADEs = [] | |
| 464 allrecs = ParDict['qTAXA'].split(',') | |
| 465 for item in allrecs: | |
| 466 if item in ['ALL', 'All', 'all']:#THIS OPTION DIAGNOSES ALL TAXA IN THE DATASET | |
| 467 qCLADEs = list(set([i[1] for i in raw_records])) | |
| 468 elif item in [i[1] for i in raw_records]: | |
| 469 qCLADEs.append(item) | |
| 470 elif '+' in item: | |
| 471 withplus.append(item) | |
| 472 elif 'VS' in item: | |
| 473 P2.append(item.split('VS')) | |
| 474 else: | |
| 475 print('UNRECOGNIZED TAXON', item) | |
| 476 #OCT2022 - end | |
| 477 print('query taxa:', len(qCLADEs+withplus), '-', str(sorted(qCLADEs)+sorted(withplus)).replace('[','').replace(']','').replace("'", ''))#1.3 | |
| 478 | |
| 479 if 'Cutoff' in list(ParDict.keys()):#CUTOFF Number of the informative positions to be considered, default 100 | |
| 480 Cutoff = ParDict['Cutoff']#VERYNEW | |
| 481 else: | |
| 482 Cutoff = 100 | |
| 483 print('Cutoff set as:', Cutoff) | |
| 484 if 'Number_of_iterations' in list(ParDict.keys()):#Number iterations of MolD | |
| 485 N1 = int(ParDict['Number_of_iterations']) | |
| 486 else: | |
| 487 N1 = 10000 | |
| 488 print('Number iterations of MolD set as:', N1) | |
| 489 | |
| 490 if 'MaxLen1' in list(ParDict.keys()):#Maximum length for the raw mDNCs | |
| 491 MaxLen1 = int(ParDict['MaxLen1']) | |
| 492 else: | |
| 493 MaxLen1 = 12 | |
| 494 print('Maximum length of raw mDNCs set as:', MaxLen1) | |
| 495 | |
| 496 if 'MaxLen2' in list(ParDict.keys()):#Maximum length for the refined mDNCs | |
| 497 MaxLen2 = int(ParDict['MaxLen2']) | |
| 498 else: | |
| 499 MaxLen2 = 7 | |
| 500 print('Maximum length of refined mDNCs set as:', MaxLen2) | |
| 501 | |
| 502 if 'Pdiff' in list(ParDict.keys()):#Percent difference | |
| 503 Percent_difference = float(ParDict['Pdiff']) | |
| 504 else: | |
| 505 if int(ParDict['Taxon_rank']) == 1:#read in taxon rank to configure Pdiff parameter of artificial dataset | |
| 506 Percent_difference = 1 | |
| 507 else: | |
| 508 Percent_difference = 5 | |
| 509 print('simulated sequences up to', Percent_difference, 'percent divergent from original ones') | |
| 510 | |
| 511 if 'NMaxSeq' in list(ParDict.keys()):#Maximum number of sequences per taxon to be modified | |
| 512 Seq_per_clade_to_screw = int(ParDict['NMaxSeq']) | |
| 513 else: | |
| 514 Seq_per_clade_to_screw = 10####!changed value | |
| 515 print('Maximum number of sequences modified per clade', Seq_per_clade_to_screw) | |
| 516 | |
| 517 if 'Scoring' in list(ParDict.keys()): | |
| 518 if ParDict['Scoring'] == 'lousy': | |
| 519 threshold = 66####!changed value | |
| 520 elif ParDict['Scoring'] == 'moderate': | |
| 521 threshold = 75####!changed value | |
| 522 elif ParDict['Scoring'] == 'stringent': | |
| 523 threshold = 90####!changed value | |
| 524 elif ParDict['Scoring'] == 'very_stringent': | |
| 525 threshold = 95####!changed value | |
| 526 else: | |
| 527 threshold = 75####!changed value | |
| 528 else: | |
| 529 threshold = 75####!changed value | |
| 530 #print(ParDict['Scoring'], 'scoring of the rDNCs; threshold in two consequtive runs:', threshold) | |
| 531 print('scoring of the rDNCs; threshold in two consequtive runs:', threshold) | |
| 532 #OCT2022 - start | |
| 533 if corr > 1 and len(ParDict['Iref'].split(',')) == 2: | |
| 534 print('\nNOTE: The alignment was trimmed automatically to match median sequences length. The analysed slice starts from the site',str(sstart+1),'and ends on the site',str(send+1),'. The site indexing in the DNCs as in the provided reference.') | |
| 535 if corr > 1 and ParDict['Iref'] == 'NO': | |
| 536 print('\nNOTE: The alignment was trimmed automatically to match median sequences length. The analysed slice starts from the site',str(sstart+1),'and ends on the site',str(send+1),'. The site indexing in the rDNC as in the untrimmed alignment.') | |
| 537 #OCT2022 - end | |
| 538 thephrase = 'The DNA diagnosis for the taxon' | |
| 539 ###################################################IMPLEMENTATION | |
| 540 #Setting up a new class just for the convenient output formatting | |
| 541 class SortedDisplayDict(dict):#this is only to get a likable formatting of the barcode | |
| 542 def __str__(self): | |
| 543 return "[" + ", ".join("%r: %r" % (key, self[key]) for key in sorted(self)) + "]" | |
| 544 | |
| 545 class SortedDisplayDictVerbose(dict):#this is only to get a likable formatting of the barcode | |
| 546 def __str__(self): | |
| 547 return ", ".join("%r %r" % (self[key],'in the site '+str(key)) for key in sorted(self)).replace("'", '').replace("A", "'A'").replace("C", "'C'").replace("G", "'G'").replace("T", "'T'")+'.' | |
| 548 | |
| 549 #Calling functions and outputing results | |
| 550 if ParDict['OUTPUT_FILE'] == "str": | |
| 551 g = StringIO() | |
| 552 else: | |
| 553 g = open(ParDict['OUTPUT_FILE'], "w")#Initiating output file | |
| 554 #VERYNEW | |
| 555 print('<h4>########################## PARAMETERS ######################</h4>', file=g) | |
| 556 #print("<p>", 'input file:', ParDict['ORIG_FNAME'], "</p>", file=g) #outcommented ORIG_FNAME | |
| 557 print("<p>", 'input file:', ParDict['INPUT_FILE'], "</p>", file=g) | |
| 558 print("<p>", 'Coding gaps as characters:', gaps2D, "</p>", file=g) | |
| 559 print("<p>", 'Maximum undetermined nucleotides allowed:', NumberN, "</p>", file=g) | |
| 560 print("<p>", 'Length of the alignment:', len(imported[0][2]),'->', FragmentLen, "</p>", file=g) | |
| 561 print("<p>", 'Indexing reference:', ParDict['Iref'].replace('NO', 'Not set').replace('in', 'included').replace('ex', 'excluded'), "</p>", file=g)#OCT2022 | |
| 562 print("<p>", 'Read in', len(raw_records), 'sequences', "</p>", file=g) | |
| 563 print("<p>", 'query taxa:', len(qCLADEs+withplus), '-', str(sorted(qCLADEs)+sorted(withplus)).replace('[','').replace(']','').replace("'", ''), "</p>", file=g)#1.3 | |
| 564 print("<p>", 'Cutoff set as:', Cutoff, "</p>", file=g) | |
| 565 print("<p>", 'Number iterations of MolD set as:', N1, "</p>", file=g) | |
| 566 print("<p>", 'Maximum length of raw mDNCs set as:', MaxLen1, "</p>", file=g) | |
| 567 print("<p>", 'Maximum length of refined mDNCs set as:', MaxLen2, "</p>", file=g) | |
| 568 print("<p>", 'simulated sequences up to', Percent_difference, 'percent divergent from original ones', "</p>", file=g) | |
| 569 print("<p>", 'Maximum number of sequences modified per clade', Seq_per_clade_to_screw, "</p>", file=g) | |
| 570 #print("<p>", ParDict['Scoring'], 'scoring of the rDNCs; threshold in two consequtive runs:', threshold, "</p>", file=g) | |
| 571 print("<p>", 'scoring of the rDNCs; threshold in two consequtive runs:', threshold, "</p>", file=g) | |
| 572 if corr > 1 and len(ParDict['Iref'].split(',')) == 2: | |
| 573 print('<h4>NOTE: The alignment was trimmed automatically to match median sequences length. The analysed slice starts from the site',str(sstart+1),'and ends on the site',str(send+1),'. The site indexing in the DNCs as in the provided reference.</h4>', file=g) | |
| 574 if corr > 1 and ParDict['Iref'] == 'NO': | |
| 575 print('<h4>NOTE: The alignment was trimmed automatically to match median sequences length. The analysed slice starts from the site',str(sstart+1),'and ends on the site',str(send+1),'. The site indexing in the rDNC as in the untrimmed alignment.</h4>', file=g) | |
| 576 | |
| 577 print('<h4>########################### RESULTS ##########################</h4>', file=g) | |
| 578 | |
| 579 for qCLADE in sorted(qCLADEs) + sorted(withplus):#OCT2022 | |
| 580 if '+' in qCLADE: | |
| 581 if shift == True: | |
| 582 old_records = raw_records | |
| 583 shift == False | |
| 584 else: | |
| 585 raw_records = old_records | |
| 586 spp = qCLADE.split('+') | |
| 587 nrecords = [] | |
| 588 for rec in raw_records: | |
| 589 if rec[1] in spp: | |
| 590 nrecords.append([rec[0], qCLADE, rec[2]]) | |
| 591 else: | |
| 592 nrecords.append(rec) | |
| 593 raw_records = nrecords | |
| 594 print('\n**************', qCLADE, '**************') | |
| 595 print('<h4>**************', qCLADE, '**************</h4>', file=g) | |
| 596 Clades, clade_sorted_seqs, shared_positions = Step1(raw_records)#STEP1 | |
| 597 x,y,z,pures = C_VP_PP(clade_sorted_seqs, qCLADE, shared_positions, Cutoff)#STEP2 ####! added pures | |
| 598 newy = {key:y[key] for key in y if not key in pures} ####! newline | |
| 599 print('Sequences analyzed:', len(clade_sorted_seqs[qCLADE])) | |
| 600 print("<p>",'Sequences analyzed:', len(clade_sorted_seqs[qCLADE]), "</p>", file=g) | |
| 601 ND_combinations = [[item] for item in pures] ####! before ND_combinations were initiated as an empty list | |
| 602 print('single nucleotide mDNCs:', len(pures), '-', str(SortedDisplayDict({pos+corr : y[pos-1] for pos in [i+1 for i in pures]}))[1:-1])#OCT2022 | |
| 603 print("<p>",'single nucleotide mDNCs*:',len(pures), '-', str(SortedDisplayDict({pos+corr : y[pos-1] for pos in [i+1 for i in pures]}))[1:-1], "</p>", file=g)#OCT2022 | |
| 604 N = 1 ####! | |
| 605 while N > 0:#STEP3 | |
| 606 try: | |
| 607 q = Diagnostic_combinations(qCLADE, x, newy, N1, MaxLen1, MaxLen2) ####! newy instead of y | |
| 608 except IndexError: | |
| 609 print(N, 'IndexError') | |
| 610 continue | |
| 611 for comb in q: | |
| 612 if not comb in ND_combinations: | |
| 613 ND_combinations.append(comb) | |
| 614 N-=1 | |
| 615 ND_combinations.sort(key=len) | |
| 616 #################################### mDNC output | |
| 617 try: | |
| 618 Nind, KeyPos = IndependentKey(ND_combinations)#STEP4 | |
| 619 except IndexError: | |
| 620 print('no mDNCs recovered for', qCLADE)#VERYNEW | |
| 621 print("<p>", 'no mDNCs recovered for', "</p>", qCLADE, file=g)#VERYNEW | |
| 622 continue | |
| 623 Allpos = []#Create list of all positions involved in mDNCs | |
| 624 for comb in ND_combinations: | |
| 625 for pos in comb: | |
| 626 if not pos in Allpos: | |
| 627 Allpos.append(pos) | |
| 628 print('\nmDNCs retrieved:', str(len(ND_combinations)) + '; Sites involved:', str(len(Allpos)) + '; Independent mDNCs:', len(Nind))#VERYNEW | |
| 629 print("<p>", 'mDNCs* retrieved:', str(len(ND_combinations)) + '; Sites involved:', str(len(Allpos)) + '; Independent mDNCs**:', len(Nind), "</p>", file=g)#VERYNEW | |
| 630 print('Shortest retrieved mDNC:', SortedDisplayDict({pos+corr : y[pos-1] for pos in [i+1 for i in ND_combinations[0]]}), '\n')#OCT2022 | |
| 631 print("<p>",'Shortest retrieved mDNC*:', SortedDisplayDict({pos+corr : y[pos-1] for pos in [i+1 for i in ND_combinations[0]]}), "</p>", file=g)#OCT2022 | |
| 632 ######################################################## rDNC output | |
| 633 Barcode_scores = []#Initiate a list for rDNC scores | |
| 634 npos = len(ND_combinations[0]) | |
| 635 BestBarcode = 'none'####! newline | |
| 636 while npos <= min([10, len(Allpos)]):#in this loop the positions are added one-by-one to a rDNC and the rDNC is then rated on the artificially generated datasets | |
| 637 Barcode = GenerateBarcode_new(ND_combinations, npos)#Initiate a rDNC | |
| 638 Barcode_score = 0#Initiate a score to rate a rDNC | |
| 639 N = 100 | |
| 640 while N > 0: | |
| 641 NComplist, NCPP = Screwed_dataset_new(raw_records, Seq_per_clade_to_screw, PosArrays, VarPosList, Percent_difference, qCLADE, Cutoff)#Create an artificial dataset VERYNEW | |
| 642 NBarcode = [i for i in Barcode if i in list(NCPP.keys())] | |
| 643 if len(Barcode) - len(NBarcode) <= 1 and ConditionD(NBarcode, NComplist, NCPP) == True:####! new condition (first) added | |
| 644 Barcode_score +=1 | |
| 645 N -=1 | |
| 646 print(npos, 'rDNC_score (100):', [k+corr+1 for k in Barcode], '-', Barcode_score)#VERYNEW | |
| 647 print("<p>", npos, 'rDNC_score (100):', [k+corr+1 for k in Barcode], '-', Barcode_score, "</p>", file=g)#OCT2022 | |
| 648 if Barcode_score >= threshold and len(Barcode_scores) == 1: ###1.3 | |
| 649 BestBarcode = Barcode###1.3 | |
| 650 if Barcode_score >= threshold and len(Barcode_scores) > 1 and Barcode_score >= max(Barcode_scores): ###1.3 | |
| 651 BestBarcode = Barcode####!newline | |
| 652 Barcode_scores.append(Barcode_score) | |
| 653 if len(Barcode_scores) >= 2 and Barcode_scores[-1] >= threshold and Barcode_scores[-2] >= threshold:#Check whether the rDNC fulfills robustnes criteria 85:85:85 | |
| 654 print('Final rDNC:', SortedDisplayDict({pos+corr : y[pos-1] for pos in [i+1 for i in BestBarcode]}))#OCT2022 | |
| 655 print("<p>",'Final rDNC***:', SortedDisplayDict({pos+corr : y[pos-1] for pos in [i+1 for i in BestBarcode]}), "</p>", file=g)#OCT2022 | |
| 656 print('\n',thephrase, qCLADE, 'is:', SortedDisplayDictVerbose({pos+corr : y[pos-1] for pos in [i+1 for i in BestBarcode]}))#OCT2022 | |
| 657 print("<p>", thephrase, qCLADE, 'is:', SortedDisplayDictVerbose({pos+corr : y[pos-1] for pos in [i+1 for i in BestBarcode]}), "</p>", file=g)#OCT2022 | |
| 658 break | |
| 659 else:# VERY NEW FROM HERE ONWARDS | |
| 660 npos += 1 | |
| 661 if npos > min([10, len(Allpos)]): | |
| 662 if BestBarcode != 'none': | |
| 663 print('The highest scoring rDNC for taxon', qCLADE, 'is:', SortedDisplayDictVerbose({pos+corr : y[pos-1] for pos in [i+1 for i in BestBarcode]}))#OCT2022 | |
| 664 print("<p>", 'The highest scoring rDNC*** for taxon', qCLADE, 'is:', SortedDisplayDictVerbose({pos+corr : y[pos-1] for pos in [i+1 for i in BestBarcode]}), "</p>", file=g)#OCT2022 | |
| 665 else: | |
| 666 print('No sufficiently robust DNA diagnosis for taxon', qCLADE, 'was retrieved') | |
| 667 print("<p>", 'No sufficiently robust DNA diagnosis for taxon', qCLADE, 'was retrieved', "</p>", file=g) | |
| 668 #OCT2022 - start | |
| 669 print("<h4>", '################################# EXPLANATIONS ####################################', "</h4>", file=g) | |
| 670 print("<p>", ' * mDNC -(=minimal Diagnostic nucleotide combination) is a combination of nucleotides at specified sites of the alignment,', "</p>", file=g) | |
| 671 print("<p>", ' unique for a query taxon. Therefore it is sufficient to differentiate a query taxon from all reference taxa in a dataset.', "</p>", file=g) | |
| 672 print("<p>", ' Because it comprises minimal necessary number of nucleotide sites to differentiate a query, any mutation in the mDNC in' "</p>", file=g) | |
| 673 print("<p>", ' single specimen of a query taxon will automatically disqualify it as a diagnostic combination.', "</p>", file=g) | |
| 674 print("<p>", "</p>", file=g) | |
| 675 print("<p>", ' ** two or more mDNCs are INDEPENDENT if they constitute non-overlapping sets of nucleotide sites.', "</p>", file=g) | |
| 676 print("<p>", "</p>", file=g) | |
| 677 print("<p>", '*** rDNC -(=robust/redundant Diagnostic nucleotide combination) is a combination of nucleotides at specified sites of the alignment,', "</p>", file=g) | |
| 678 print("<p>", ' unique for a query taxon and (likewise mDNC) sufficient to differentiate a query taxon from all reference taxa in a dataset.', "</p>", file=g) | |
| 679 print("<p>", ' However, rDNC comprises more than a minimal necessary number of diagnostic sites, and therefore is robust to single nucleotide', "</p>", file=g) | |
| 680 print("<p>", ' replacements. Even if a mutation arises in one of the rDNC sites, the remaining ones will (with high probability) remain sufficient ', "</p>", file=g) | |
| 681 print("<p>", ' to diagnose the query taxon', "</p>", file=g) | |
| 682 print("<h4>", ' Final diagnosis corresponds to rDNC', "</h4>", file=g) | |
| 683 #OCT2022 - end | |
| 684 | |
| 685 if ParDict['OUTPUT_FILE'] == "str": | |
| 686 contents = g.getvalue() | |
| 687 os.unlink(ParDict['INPUT_FILE']) | |
| 688 else: | |
| 689 contents = None | |
| 690 g.close() | |
| 691 #OCT2022 - start | |
| 692 if len(P2) != 0: | |
| 693 ext = '.'+ParDict['OUTPUT_FILE'].split('.')[-1] | |
| 694 h = open(ParDict['OUTPUT_FILE'].replace(ext, '_pairwise'+ext), "w")#Initiating output file | |
| 695 if len(withplus) != 0: | |
| 696 raw_records = old_records | |
| 697 taxain = [i[1] for i in raw_records] | |
| 698 tpairs = [] | |
| 699 for alist in P2: | |
| 700 if alist.count('all')+alist.count('All')+alist.count('ALL') == 2: | |
| 701 tpairs = getAllPairs(taxain) | |
| 702 elif alist.count('all')+alist.count('All')+alist.count('ALL') == 1: | |
| 703 thetax = [i for i in taxain if i in alist][0] | |
| 704 print(thetax) | |
| 705 for atax in sorted(list(set(taxain))): | |
| 706 if atax != thetax: | |
| 707 tpairs.append([thetax, atax]) | |
| 708 else: | |
| 709 for apair in getAllPairs(alist): | |
| 710 tpairs.append(apair) | |
| 711 for apair in tpairs: | |
| 712 t1 = apair[0] | |
| 713 t2 = apair[1] | |
| 714 p2records = [i for i in raw_records if i[1] in [t1, t2]] | |
| 715 print('\n**************', t1, 'VS', t2,'**************') | |
| 716 print('<h4>**************', t1, 'VS', t2, '**************</h4>', file=h) | |
| 717 C2, css2, sp2 = Step1(p2records)#STEP1 | |
| 718 x2,y2,z2,pures2 = C_VP_PP(css2, t1, sp2, '>0')#STEP2 ####! added pures | |
| 719 Pairphrase = 'Each of the following '+ str(len(pures2))+' sites is invariant across sequences of '+ t1+ ' and differentiates it from '+ t2+': ' | |
| 720 counterPures = {} | |
| 721 for site in pures2: | |
| 722 counterPures[site] = "'or'".join(list(set([thing[site] for thing in css2[t2] if thing[site] != 'N']))) | |
| 723 Pairphrase = Pairphrase + str(site+corr)+" ('"+str(y2[site])+"' vs '"+str(counterPures[site])+"'), " | |
| 724 print(Pairphrase[:-2]) | |
| 725 print("<p>",Pairphrase[:-2],'</h4>', file=h)#OCT2022 | |
| 726 x2r,y2r,z2r,pures2r = C_VP_PP(css2, t2, sp2, '>0')#STEP2 ####! added pures | |
| 727 Pairphraser = 'Each of the following '+ str(len(pures2r))+' sites is invariant across sequences of '+ t2+ ' and differentiates it from '+ t1+': ' | |
| 728 counterPuresr = {} | |
| 729 for site in pures2r: | |
| 730 counterPuresr[site] = "'or'".join(list(set([thing[site] for thing in css2[t1] if thing[site] != 'N']))) | |
| 731 Pairphraser = Pairphraser + str(site+corr)+" ('"+str(y2r[site])+"' vs '"+str(counterPuresr[site])+"'), " | |
| 732 print(Pairphraser[:-2]) | |
| 733 print("<p>",Pairphraser[:-2],'</h4>', file=h)#OCT2022 | |
| 734 h.close() | |
| 735 | |
| 736 #OCT2022 - end | |
| 737 return contents, qCLADEs | |
| 738 if __name__ == "__main__": | |
| 739 c, q = mainprocessing() | |
| 740 |
