ArraySplitter 1.3.0__py3-none-any.whl → 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ArraySplitter/__init__.py CHANGED
@@ -5,6 +5,6 @@
5
5
  # @author: Aleksey Komissarov
6
6
  # @contact: ad3002@gmail.com
7
7
 
8
- __version__ = "1.3.0"
8
+ __version__ = "1.4.0"
9
9
 
10
10
  # __all__ = []
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Indel Chromosome Specificity Analysis.
5
+
6
+ Analyzes how chromosome-specific indels are in MSA alignments.
7
+ For each indel, calculates:
8
+ - Which chromosomes contain sequences with this indel
9
+ - What fraction of sequences from each chromosome have the indel
10
+ - Overall specificity score
11
+ """
12
+
13
+ import argparse
14
+ from collections import defaultdict
15
+ from typing import List, Tuple, Dict, Set
16
+ from dataclasses import dataclass
17
+ import re
18
+ import json
19
+
20
+
21
+ @dataclass
22
+ class Indel:
23
+ start: int
24
+ end: int
25
+ length: int
26
+
27
+ def __hash__(self):
28
+ return hash((self.start, self.end))
29
+
30
+ def __eq__(self, other):
31
+ return self.start == other.start and self.end == other.end
32
+
33
+
34
+ def parse_fasta(filepath: str) -> List[Tuple[str, str]]:
35
+ """Parse FASTA file."""
36
+ sequences = []
37
+ with open(filepath) as f:
38
+ name = ""
39
+ seq = ""
40
+ for line in f:
41
+ line = line.strip()
42
+ if line.startswith(">"):
43
+ if seq:
44
+ sequences.append((name, seq))
45
+ name = line[1:].split()[0]
46
+ seq = ""
47
+ else:
48
+ seq += line
49
+ if seq:
50
+ sequences.append((name, seq))
51
+ return sequences
52
+
53
+
54
+ def extract_chromosome(name: str) -> str:
55
+ """Extract chromosome name from sequence header."""
56
+ # Pattern: chr1_mat:123-456|... or chr1A_pat:...
57
+ match = re.match(r'(chr[^:]+)', name)
58
+ if match:
59
+ return match.group(1)
60
+ return "unknown"
61
+
62
+
63
+ def find_indels_in_sequence(seq: str) -> List[Indel]:
64
+ """Find all indel regions in a sequence."""
65
+ indels = []
66
+ in_gap = False
67
+ gap_start = 0
68
+
69
+ for i, base in enumerate(seq):
70
+ if base == '-':
71
+ if not in_gap:
72
+ in_gap = True
73
+ gap_start = i
74
+ else:
75
+ if in_gap:
76
+ indels.append(Indel(start=gap_start, end=i, length=i - gap_start))
77
+ in_gap = False
78
+
79
+ if in_gap:
80
+ indels.append(Indel(start=gap_start, end=len(seq), length=len(seq) - gap_start))
81
+
82
+ return indels
83
+
84
+
85
+ def analyze_chromosome_specificity(input_fasta: str, output_tsv: str, output_json: str,
86
+ min_count: int = 3, verbose: bool = False) -> Dict:
87
+ """
88
+ Analyze chromosome specificity of indels.
89
+
90
+ Args:
91
+ input_fasta: Aligned FASTA file
92
+ output_tsv: Output TSV with detailed analysis
93
+ output_json: Output JSON for HTML report
94
+ min_count: Minimum sequence count for indel to be included
95
+ verbose: Print progress
96
+ """
97
+ sequences = parse_fasta(input_fasta)
98
+ n_seqs = len(sequences)
99
+
100
+ if verbose:
101
+ print(f"Loaded {n_seqs} sequences")
102
+
103
+ # Count sequences per chromosome
104
+ chr_seq_counts: Dict[str, int] = defaultdict(int)
105
+ for name, _ in sequences:
106
+ chrom = extract_chromosome(name)
107
+ chr_seq_counts[chrom] += 1
108
+
109
+ if verbose:
110
+ print(f"Found {len(chr_seq_counts)} chromosomes")
111
+
112
+ # Map indels to chromosomes and sequences
113
+ indel_to_chr_seqs: Dict[Indel, Dict[str, List[str]]] = defaultdict(lambda: defaultdict(list))
114
+
115
+ for name, seq in sequences:
116
+ chrom = extract_chromosome(name)
117
+ indels = find_indels_in_sequence(seq)
118
+ for indel in indels:
119
+ indel_to_chr_seqs[indel][chrom].append(name)
120
+
121
+ # Analyze each indel
122
+ results = []
123
+
124
+ for indel, chr_seqs in indel_to_chr_seqs.items():
125
+ total_count = sum(len(seqs) for seqs in chr_seqs.values())
126
+
127
+ # Skip rare indels
128
+ if total_count < min_count:
129
+ continue
130
+
131
+ # Calculate per-chromosome statistics
132
+ chr_stats = []
133
+ for chrom, seq_names in chr_seqs.items():
134
+ count_in_chr = len(seq_names)
135
+ total_in_chr = chr_seq_counts[chrom]
136
+ fraction = count_in_chr / total_in_chr if total_in_chr > 0 else 0
137
+ chr_stats.append({
138
+ "chr": chrom,
139
+ "count": count_in_chr,
140
+ "total": total_in_chr,
141
+ "fraction": fraction
142
+ })
143
+
144
+ # Sort by fraction descending
145
+ chr_stats.sort(key=lambda x: -x["fraction"])
146
+
147
+ # Calculate specificity metrics
148
+ n_chromosomes = len(chr_stats)
149
+ max_fraction = chr_stats[0]["fraction"] if chr_stats else 0
150
+
151
+ # Specificity score: high if indel is concentrated in few chromosomes
152
+ # If indel appears in all sequences of 1-2 chromosomes, it's highly specific
153
+ top_chr_contribution = sum(s["count"] for s in chr_stats[:2]) / total_count if total_count > 0 else 0
154
+
155
+ # Is this indel chromosome-specific?
156
+ # Criteria: >80% of occurrences from top 2 chromosomes AND top chr has >50% of its sequences with indel
157
+ is_chr_specific = top_chr_contribution > 0.8 and max_fraction > 0.5
158
+
159
+ results.append({
160
+ "start": indel.start,
161
+ "end": indel.end,
162
+ "length": indel.length,
163
+ "total_count": total_count,
164
+ "frequency": total_count / n_seqs,
165
+ "n_chromosomes": n_chromosomes,
166
+ "max_fraction": max_fraction,
167
+ "top_chr_contribution": top_chr_contribution,
168
+ "is_chr_specific": is_chr_specific,
169
+ "chr_stats": chr_stats
170
+ })
171
+
172
+ # Sort by total count descending
173
+ results.sort(key=lambda x: -x["total_count"])
174
+
175
+ # Summary statistics
176
+ n_total_indels = len(results)
177
+ n_chr_specific = sum(1 for r in results if r["is_chr_specific"])
178
+
179
+ # Write TSV
180
+ with open(output_tsv, 'w') as f:
181
+ f.write("# Indel Chromosome Specificity Analysis\n")
182
+ f.write(f"# Input: {input_fasta}\n")
183
+ f.write(f"# Total sequences: {n_seqs}\n")
184
+ f.write(f"# Chromosomes: {len(chr_seq_counts)}\n")
185
+ f.write(f"# Indels analyzed (count >= {min_count}): {n_total_indels}\n")
186
+ f.write(f"# Chromosome-specific indels: {n_chr_specific} ({n_chr_specific/n_total_indels*100:.1f}%)\n")
187
+ f.write("#\n")
188
+ f.write("start\tend\tlength\ttotal_count\tfrequency\tn_chromosomes\tmax_chr_fraction\ttop2_contribution\tis_specific\ttop_chromosomes\n")
189
+
190
+ for r in results:
191
+ top_chrs = ", ".join(f"{s['chr']}({s['count']}/{s['total']}={s['fraction']*100:.0f}%)"
192
+ for s in r["chr_stats"][:5])
193
+ f.write(f"{r['start']}\t{r['end']}\t{r['length']}\t{r['total_count']}\t"
194
+ f"{r['frequency']*100:.2f}%\t{r['n_chromosomes']}\t{r['max_fraction']*100:.1f}%\t"
195
+ f"{r['top_chr_contribution']*100:.1f}%\t{'YES' if r['is_chr_specific'] else 'no'}\t{top_chrs}\n")
196
+
197
+ # Write JSON for HTML report
198
+ json_data = {
199
+ "summary": {
200
+ "total_sequences": n_seqs,
201
+ "n_chromosomes": len(chr_seq_counts),
202
+ "n_indels_analyzed": n_total_indels,
203
+ "n_chr_specific": n_chr_specific,
204
+ "pct_chr_specific": round(n_chr_specific / n_total_indels * 100, 1) if n_total_indels > 0 else 0
205
+ },
206
+ "chromosome_counts": dict(chr_seq_counts),
207
+ "indels": results[:50] # Top 50 for HTML
208
+ }
209
+
210
+ with open(output_json, 'w') as f:
211
+ json.dump(json_data, f, indent=2)
212
+
213
+ if verbose:
214
+ print(f"\nResults:")
215
+ print(f" Indels analyzed: {n_total_indels}")
216
+ print(f" Chromosome-specific: {n_chr_specific} ({n_chr_specific/n_total_indels*100:.1f}%)")
217
+ print(f"\nOutput: {output_tsv}, {output_json}")
218
+
219
+ return json_data
220
+
221
+
222
+ def main():
223
+ parser = argparse.ArgumentParser(description="Analyze chromosome specificity of indels")
224
+ parser.add_argument("-i", "--input", required=True, help="Aligned FASTA file")
225
+ parser.add_argument("-o", "--output", required=True, help="Output prefix")
226
+ parser.add_argument("-m", "--min-count", type=int, default=3,
227
+ help="Minimum count for indel to be included (default: 3)")
228
+ parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
229
+
230
+ args = parser.parse_args()
231
+
232
+ analyze_chromosome_specificity(
233
+ input_fasta=args.input,
234
+ output_tsv=f"{args.output}.chr_specificity.tsv",
235
+ output_json=f"{args.output}.chr_specificity.json",
236
+ min_count=args.min_count,
237
+ verbose=args.verbose
238
+ )
239
+
240
+
241
+ if __name__ == "__main__":
242
+ main()
ArraySplitter/main.py CHANGED
@@ -31,6 +31,12 @@ Examples:
31
31
  # Extract unique monomers
32
32
  arraysplitter extract -i decomposed.fa -o monomers
33
33
 
34
+ # Refine decomposition (split merged, join fragments)
35
+ arraysplitter refine -i decomposed.fa -o refined.fa
36
+
37
+ # Refine MSA by removing sequences with rare indels
38
+ arraysplitter msa_refine -i aligned.fa -o cleaned
39
+
34
40
  For help on specific commands:
35
41
  arraysplitter <command> -h
36
42
  """
@@ -122,7 +128,51 @@ For help on specific commands:
122
128
  type=int,
123
129
  default=1
124
130
  )
125
-
131
+
132
+ # Refine command
133
+ parser_refine = subparsers.add_parser(
134
+ 'refine',
135
+ help='Refine decomposition: split merged monomers, join fragments'
136
+ )
137
+ parser_refine.add_argument("-i", "--input", help="Input .monomers.tsv file", required=True)
138
+ parser_refine.add_argument("-o", "--output", help="Output .monomers.tsv file (refined)", required=True)
139
+ parser_refine.add_argument(
140
+ "-l", "--length",
141
+ help="Expected monomer length (auto-detected if not specified)",
142
+ type=int,
143
+ default=None
144
+ )
145
+ parser_refine.add_argument(
146
+ "-m", "--mismatches",
147
+ help="Maximum mismatches for cut sequence matching (default: 3)",
148
+ type=int,
149
+ default=3
150
+ )
151
+ parser_refine.add_argument(
152
+ "-v", "--verbose",
153
+ help="Verbose output",
154
+ action="store_true"
155
+ )
156
+
157
+ # MSA Refine command
158
+ parser_msa_refine = subparsers.add_parser(
159
+ 'msa_refine',
160
+ help='Remove sequences with rare indels from MSA'
161
+ )
162
+ parser_msa_refine.add_argument("-i", "--input", help="Input aligned FASTA file", required=True)
163
+ parser_msa_refine.add_argument("-o", "--output", help="Output prefix", required=True)
164
+ parser_msa_refine.add_argument(
165
+ "-f", "--min-freq",
166
+ help="Minimum indel frequency threshold (0-1, default: 0.05 = 5%%)",
167
+ type=float,
168
+ default=0.05
169
+ )
170
+ parser_msa_refine.add_argument(
171
+ "-v", "--verbose",
172
+ help="Verbose output",
173
+ action="store_true"
174
+ )
175
+
126
176
  # Parse arguments
127
177
  args = parser.parse_args()
128
178
 
@@ -178,9 +228,50 @@ For help on specific commands:
178
228
 
179
229
  elif args.command == 'extract':
180
230
  from .extract import main as extract_main
181
-
231
+
182
232
  extract_main(args.fasta, args.output, min_tf=args.min)
183
-
233
+
234
+ elif args.command == 'refine':
235
+ from .refine import main as refine_main
236
+ import os
237
+
238
+ if not os.path.isfile(args.input):
239
+ print(f"Error: Input file {args.input} not found")
240
+ sys.exit(1)
241
+
242
+ refine_main(
243
+ input_tsv=args.input,
244
+ output_tsv=args.output,
245
+ expected_length=args.length,
246
+ max_mismatches=args.mismatches,
247
+ verbose=args.verbose
248
+ )
249
+
250
+ elif args.command == 'msa_refine':
251
+ from .msa_refine import refine_msa
252
+ import os
253
+
254
+ if not os.path.isfile(args.input):
255
+ print(f"Error: Input file {args.input} not found")
256
+ sys.exit(1)
257
+
258
+ output_clean = f"{args.output}.clean.fasta"
259
+ output_removed = f"{args.output}.removed.fasta"
260
+ output_stats = f"{args.output}.indel_stats.tsv"
261
+
262
+ stats = refine_msa(
263
+ input_fasta=args.input,
264
+ output_clean=output_clean,
265
+ output_removed=output_removed,
266
+ output_stats=output_stats,
267
+ min_freq=args.min_freq,
268
+ verbose=args.verbose
269
+ )
270
+
271
+ print(f"\nRefinement complete:")
272
+ print(f" Total: {stats['total']} -> Clean: {stats['clean']}, Removed: {stats['removed']}")
273
+ print(f" Indels: {stats['total_indels']} total, {stats['rare_indels']} rare (< {stats['min_freq']*100:.1f}%)")
274
+
184
275
  else:
185
276
  print(f"Error: Unknown command '{args.command}'")
186
277
  parser.print_help()
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ ArraySplitter Monomer Rotation - Rotate monomers to common starting position.
5
+
6
+ This module finds the optimal rotation k-mer and rotates all monomers to start
7
+ from that position. Important for tandem repeats: the sequence is duplicated
8
+ before searching to handle cases where the k-mer spans the boundary.
9
+
10
+ Algorithm:
11
+ 1. Find k-mer that appears exactly ONCE in the maximum number of monomers
12
+ 2. Duplicate each sequence (seq+seq) to handle boundary cases
13
+ 3. Find best match position using edit distance
14
+ 4. Rotate sequence to start from that position
15
+ 5. Ensure all sequences are on the same strand
16
+ """
17
+
18
+ import argparse
19
+ from collections import Counter
20
+ from typing import List, Tuple, Dict, Optional
21
+ import editdistance as ed
22
+
23
+
24
+ def get_revcomp(seq: str) -> str:
25
+ """Get reverse complement of sequence."""
26
+ complement = {'A': 'T', 'T': 'A', 'C': 'G', 'G': 'C', 'N': 'N'}
27
+ return ''.join(complement.get(b, b) for b in reversed(seq))
28
+
29
+
30
+ def find_best_rotation_kmer(sequences: List[str], k: int = 16) -> Tuple[Optional[str], float]:
31
+ """
32
+ Find k-mer that appears exactly once in the maximum number of sequences.
33
+
34
+ Args:
35
+ sequences: List of DNA sequences (monomers)
36
+ k: K-mer length (default 16)
37
+
38
+ Returns:
39
+ (best_kmer, fraction_unique) - k-mer and fraction of sequences with unique occurrence
40
+ """
41
+ kmer_unique_count: Counter = Counter()
42
+
43
+ for seq in sequences:
44
+ # Count k-mers in this sequence
45
+ seq_kmers: Counter = Counter()
46
+ for i in range(len(seq) - k + 1):
47
+ kmer = seq[i:i+k]
48
+ seq_kmers[kmer] += 1
49
+
50
+ # Record k-mers that appear exactly once
51
+ for kmer, count in seq_kmers.items():
52
+ if count == 1:
53
+ kmer_unique_count[kmer] += 1
54
+
55
+ if not kmer_unique_count:
56
+ return None, 0.0
57
+
58
+ best_kmer, best_count = kmer_unique_count.most_common(1)[0]
59
+ fraction = best_count / len(sequences)
60
+
61
+ return best_kmer, fraction
62
+
63
+
64
+ def find_kmer_position_in_doubled(seq: str, kmer: str, max_ed: int = 5) -> Tuple[int, int, str]:
65
+ """
66
+ Find best position of k-mer in doubled sequence (seq+seq) to handle boundary cases.
67
+ Checks both strands.
68
+
69
+ Args:
70
+ seq: Original sequence
71
+ kmer: K-mer to find
72
+ max_ed: Maximum edit distance allowed
73
+
74
+ Returns:
75
+ (position, edit_distance, strand) where position is in original [0, len(seq))
76
+ """
77
+ k = len(kmer)
78
+ seq_len = len(seq)
79
+
80
+ # Double the sequence to handle boundary
81
+ doubled_fwd = seq + seq
82
+ doubled_rev = get_revcomp(seq) + get_revcomp(seq)
83
+
84
+ best_pos = -1
85
+ best_dist = max_ed + 1
86
+ best_strand = "unk"
87
+
88
+ # Search in forward strand (doubled)
89
+ for i in range(seq_len): # Only search first copy positions
90
+ window = doubled_fwd[i:i+k]
91
+ dist = ed.eval(kmer, window)
92
+ if dist < best_dist:
93
+ best_dist = dist
94
+ best_pos = i
95
+ best_strand = "fwd"
96
+
97
+ # Search in reverse complement (doubled)
98
+ for i in range(seq_len):
99
+ window = doubled_rev[i:i+k]
100
+ dist = ed.eval(kmer, window)
101
+ if dist < best_dist:
102
+ best_dist = dist
103
+ best_pos = i
104
+ best_strand = "rev"
105
+
106
+ if best_dist <= max_ed:
107
+ return best_pos, best_dist, best_strand
108
+
109
+ return -1, best_dist, "unk"
110
+
111
+
112
+ def rotate_sequence(seq: str, position: int, strand: str) -> str:
113
+ """
114
+ Rotate sequence to start from given position.
115
+
116
+ Args:
117
+ seq: Original sequence
118
+ position: Position to start from
119
+ strand: "fwd" or "rev" - which strand to use
120
+
121
+ Returns:
122
+ Rotated sequence
123
+ """
124
+ if strand == "rev":
125
+ seq = get_revcomp(seq)
126
+
127
+ return seq[position:] + seq[:position]
128
+
129
+
130
+ def rotate_monomers(input_fasta: str, output_fasta: str,
131
+ kmer: Optional[str] = None, k: int = 16,
132
+ max_ed: int = 5, verbose: bool = False) -> Dict:
133
+ """
134
+ Main rotation function.
135
+
136
+ Args:
137
+ input_fasta: Input FASTA file with monomers
138
+ output_fasta: Output FASTA file with rotated monomers
139
+ kmer: Optional rotation k-mer (auto-detected if not provided)
140
+ k: K-mer length for auto-detection
141
+ max_ed: Maximum edit distance for rotation
142
+ verbose: Print progress
143
+
144
+ Returns:
145
+ Statistics dictionary
146
+ """
147
+ # Read sequences
148
+ sequences = []
149
+ headers = []
150
+
151
+ with open(input_fasta) as f:
152
+ seq = ""
153
+ header = ""
154
+ for line in f:
155
+ if line.startswith(">"):
156
+ if seq:
157
+ sequences.append(seq)
158
+ headers.append(header)
159
+ seq = ""
160
+ header = line.strip()
161
+ else:
162
+ seq += line.strip()
163
+ if seq:
164
+ sequences.append(seq)
165
+ headers.append(header)
166
+
167
+ if verbose:
168
+ print(f"Loaded {len(sequences)} sequences")
169
+
170
+ # Auto-detect rotation k-mer if not provided
171
+ if kmer is None:
172
+ # Filter to normal-length monomers for k-mer detection
173
+ normal_seqs = [s for s in sequences if 600 <= len(s) <= 800]
174
+ if not normal_seqs:
175
+ normal_seqs = sequences
176
+
177
+ kmer, fraction = find_best_rotation_kmer(normal_seqs, k)
178
+
179
+ if verbose:
180
+ print(f"Auto-detected rotation k-mer: {kmer}")
181
+ print(f" Unique in {fraction*100:.1f}% of monomers")
182
+
183
+ if kmer is None:
184
+ raise ValueError("Could not detect rotation k-mer")
185
+
186
+ # Rotate all sequences
187
+ rotated = []
188
+ stats = {
189
+ "total": len(sequences),
190
+ "fwd": 0,
191
+ "rev": 0,
192
+ "failed": 0,
193
+ "ed_distribution": Counter()
194
+ }
195
+
196
+ for header, seq in zip(headers, sequences):
197
+ pos, dist, strand = find_kmer_position_in_doubled(seq, kmer, max_ed)
198
+
199
+ if pos >= 0:
200
+ rotated_seq = rotate_sequence(seq, pos, strand)
201
+ stats[strand] += 1
202
+ stats["ed_distribution"][dist] += 1
203
+ else:
204
+ # Keep original if rotation failed
205
+ rotated_seq = seq
206
+ strand = "failed"
207
+ stats["failed"] += 1
208
+
209
+ rotated.append((header, rotated_seq, strand, dist))
210
+
211
+ # Write output
212
+ with open(output_fasta, 'w') as f:
213
+ for header, seq, strand, dist in rotated:
214
+ new_header = f"{header}|rot_{strand}|ed{dist}"
215
+ f.write(f"{new_header}\n{seq}\n")
216
+
217
+ if verbose:
218
+ print(f"\nRotation results:")
219
+ print(f" Forward: {stats['fwd']}")
220
+ print(f" Reverse: {stats['rev']}")
221
+ print(f" Failed: {stats['failed']}")
222
+ print(f"\nEdit distance distribution:")
223
+ for d, c in sorted(stats["ed_distribution"].items()):
224
+ print(f" ED={d}: {c}")
225
+
226
+ stats["kmer"] = kmer
227
+ return stats
228
+
229
+
230
+ def main():
231
+ parser = argparse.ArgumentParser(
232
+ description="Rotate monomers to common starting position"
233
+ )
234
+ parser.add_argument("-i", "--input", required=True,
235
+ help="Input FASTA file with monomers")
236
+ parser.add_argument("-o", "--output", required=True,
237
+ help="Output FASTA file with rotated monomers")
238
+ parser.add_argument("-k", "--kmer", default=None,
239
+ help="Rotation k-mer (auto-detected if not provided)")
240
+ parser.add_argument("-l", "--kmer-length", type=int, default=16,
241
+ help="K-mer length for auto-detection (default: 16)")
242
+ parser.add_argument("-e", "--max-ed", type=int, default=5,
243
+ help="Maximum edit distance for rotation (default: 5)")
244
+ parser.add_argument("-v", "--verbose", action="store_true",
245
+ help="Verbose output")
246
+
247
+ args = parser.parse_args()
248
+
249
+ stats = rotate_monomers(
250
+ input_fasta=args.input,
251
+ output_fasta=args.output,
252
+ kmer=args.kmer,
253
+ k=args.kmer_length,
254
+ max_ed=args.max_ed,
255
+ verbose=args.verbose
256
+ )
257
+
258
+ print(f"Rotation complete. K-mer used: {stats['kmer']}")
259
+
260
+
261
+ if __name__ == "__main__":
262
+ main()
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ MSA Refine - Remove sequences with rare indels from multiple sequence alignment.
5
+
6
+ This module analyzes indel patterns in MSA and removes sequences containing
7
+ rare indels (below frequency threshold). This helps clean up alignments
8
+ before final consensus generation.
9
+
10
+ Algorithm:
11
+ 1. Parse aligned FASTA and identify all indel patterns (contiguous gaps)
12
+ 2. Calculate frequency of each indel pattern across all sequences
13
+ 3. Mark sequences containing rare indels (frequency < threshold)
14
+ 4. Output: cleaned sequences, removed sequences, statistics report
15
+ """
16
+
17
+ import argparse
18
+ from collections import Counter, defaultdict
19
+ from typing import List, Tuple, Dict, Set
20
+ from dataclasses import dataclass
21
+ import re
22
+
23
+
24
+ @dataclass
25
+ class Indel:
26
+ """Represents an indel (gap region) in the alignment."""
27
+ start: int
28
+ end: int
29
+ length: int
30
+
31
+ def __hash__(self):
32
+ return hash((self.start, self.end))
33
+
34
+ def __eq__(self, other):
35
+ return self.start == other.start and self.end == other.end
36
+
37
+
38
+ @dataclass
39
+ class IndelStats:
40
+ """Statistics for an indel pattern."""
41
+ indel: Indel
42
+ count: int
43
+ frequency: float
44
+ sequences: List[str] # sequence names containing this indel
45
+
46
+
47
+ def parse_fasta(filepath: str) -> List[Tuple[str, str]]:
48
+ """Parse FASTA file, return list of (name, sequence) tuples."""
49
+ sequences = []
50
+ with open(filepath) as f:
51
+ name = ""
52
+ seq = ""
53
+ for line in f:
54
+ line = line.strip()
55
+ if line.startswith(">"):
56
+ if seq:
57
+ sequences.append((name, seq))
58
+ name = line[1:].split()[0] # Take first word as name
59
+ seq = ""
60
+ else:
61
+ seq += line
62
+ if seq:
63
+ sequences.append((name, seq))
64
+ return sequences
65
+
66
+
67
+ def find_indels_in_sequence(seq: str) -> List[Indel]:
68
+ """Find all indel (gap) regions in a sequence."""
69
+ indels = []
70
+ in_gap = False
71
+ gap_start = 0
72
+
73
+ for i, base in enumerate(seq):
74
+ if base == '-':
75
+ if not in_gap:
76
+ in_gap = True
77
+ gap_start = i
78
+ else:
79
+ if in_gap:
80
+ indels.append(Indel(start=gap_start, end=i, length=i - gap_start))
81
+ in_gap = False
82
+
83
+ # Handle gap at end
84
+ if in_gap:
85
+ indels.append(Indel(start=gap_start, end=len(seq), length=len(seq) - gap_start))
86
+
87
+ return indels
88
+
89
+
90
+ def analyze_indels(sequences: List[Tuple[str, str]]) -> Dict[Indel, IndelStats]:
91
+ """Analyze all indels across sequences and compute statistics."""
92
+ indel_to_seqs: Dict[Indel, List[str]] = defaultdict(list)
93
+ n_seqs = len(sequences)
94
+
95
+ for name, seq in sequences:
96
+ indels = find_indels_in_sequence(seq)
97
+ for indel in indels:
98
+ indel_to_seqs[indel].append(name)
99
+
100
+ # Compute statistics
101
+ stats = {}
102
+ for indel, seq_names in indel_to_seqs.items():
103
+ count = len(seq_names)
104
+ stats[indel] = IndelStats(
105
+ indel=indel,
106
+ count=count,
107
+ frequency=count / n_seqs,
108
+ sequences=seq_names
109
+ )
110
+
111
+ return stats
112
+
113
+
114
+ def refine_msa(
115
+ input_fasta: str,
116
+ output_clean: str,
117
+ output_removed: str,
118
+ output_stats: str,
119
+ min_freq: float = 0.05,
120
+ verbose: bool = False
121
+ ) -> Dict:
122
+ """
123
+ Refine MSA by removing sequences with rare indels.
124
+
125
+ Args:
126
+ input_fasta: Input aligned FASTA file
127
+ output_clean: Output FASTA with cleaned sequences
128
+ output_removed: Output FASTA with removed sequences
129
+ output_stats: Output TSV with indel statistics
130
+ min_freq: Minimum frequency threshold for indels (default 5%)
131
+ verbose: Print progress
132
+
133
+ Returns:
134
+ Statistics dictionary
135
+ """
136
+ # Parse sequences
137
+ sequences = parse_fasta(input_fasta)
138
+ n_total = len(sequences)
139
+
140
+ if verbose:
141
+ print(f"Loaded {n_total} aligned sequences")
142
+
143
+ # Analyze indels
144
+ indel_stats = analyze_indels(sequences)
145
+
146
+ if verbose:
147
+ print(f"Found {len(indel_stats)} unique indel patterns")
148
+
149
+ # Find rare indels and sequences to remove
150
+ rare_indels = {indel: stats for indel, stats in indel_stats.items()
151
+ if stats.frequency < min_freq}
152
+
153
+ # Collect sequences to remove (those with ANY rare indel)
154
+ seqs_to_remove: Set[str] = set()
155
+ for stats in rare_indels.values():
156
+ seqs_to_remove.update(stats.sequences)
157
+
158
+ if verbose:
159
+ print(f"Found {len(rare_indels)} rare indels (freq < {min_freq*100:.1f}%)")
160
+ print(f"Sequences to remove: {len(seqs_to_remove)}")
161
+
162
+ # Split sequences
163
+ clean_seqs = []
164
+ removed_seqs = []
165
+
166
+ for name, seq in sequences:
167
+ if name in seqs_to_remove:
168
+ removed_seqs.append((name, seq))
169
+ else:
170
+ clean_seqs.append((name, seq))
171
+
172
+ # Write clean sequences (remove columns that are now all gaps)
173
+ if clean_seqs:
174
+ # Find columns that have at least one non-gap
175
+ align_len = len(clean_seqs[0][1])
176
+ non_gap_cols = []
177
+ for i in range(align_len):
178
+ has_base = any(seq[i] != '-' for _, seq in clean_seqs)
179
+ if has_base:
180
+ non_gap_cols.append(i)
181
+
182
+ # Write cleaned sequences with gap-only columns removed
183
+ with open(output_clean, 'w') as f:
184
+ for name, seq in clean_seqs:
185
+ cleaned_seq = ''.join(seq[i] for i in non_gap_cols)
186
+ f.write(f">{name}\n{cleaned_seq}\n")
187
+
188
+ cols_removed = align_len - len(non_gap_cols)
189
+ if verbose and cols_removed > 0:
190
+ print(f"Removed {cols_removed} gap-only columns")
191
+
192
+ # Write removed sequences
193
+ with open(output_removed, 'w') as f:
194
+ for name, seq in removed_seqs:
195
+ f.write(f">{name}\n{seq}\n")
196
+
197
+ # Write statistics
198
+ with open(output_stats, 'w') as f:
199
+ f.write("# MSA Refinement Statistics\n")
200
+ f.write(f"# Input: {input_fasta}\n")
201
+ f.write(f"# Total sequences: {n_total}\n")
202
+ f.write(f"# Clean sequences: {len(clean_seqs)}\n")
203
+ f.write(f"# Removed sequences: {len(removed_seqs)}\n")
204
+ f.write(f"# Min frequency threshold: {min_freq*100:.1f}%\n")
205
+ f.write(f"# Unique indel patterns: {len(indel_stats)}\n")
206
+ f.write(f"# Rare indels (below threshold): {len(rare_indels)}\n")
207
+ f.write("#\n")
208
+ f.write("# Indel details (sorted by frequency):\n")
209
+ f.write("start\tend\tlength\tcount\tfrequency\tis_rare\texample_sequences\n")
210
+
211
+ # Sort by frequency descending
212
+ sorted_indels = sorted(indel_stats.items(),
213
+ key=lambda x: (-x[1].frequency, x[0].start))
214
+
215
+ for indel, stats in sorted_indels:
216
+ is_rare = "RARE" if stats.frequency < min_freq else ""
217
+ example_seqs = ",".join(stats.sequences[:3])
218
+ if len(stats.sequences) > 3:
219
+ example_seqs += f"... (+{len(stats.sequences)-3} more)"
220
+ f.write(f"{indel.start}\t{indel.end}\t{indel.length}\t"
221
+ f"{stats.count}\t{stats.frequency*100:.2f}%\t{is_rare}\t{example_seqs}\n")
222
+
223
+ if verbose:
224
+ print(f"\nOutput files:")
225
+ print(f" Clean sequences: {output_clean}")
226
+ print(f" Removed sequences: {output_removed}")
227
+ print(f" Statistics: {output_stats}")
228
+
229
+ return {
230
+ "total": n_total,
231
+ "clean": len(clean_seqs),
232
+ "removed": len(removed_seqs),
233
+ "total_indels": len(indel_stats),
234
+ "rare_indels": len(rare_indels),
235
+ "min_freq": min_freq
236
+ }
237
+
238
+
239
+ def main():
240
+ parser = argparse.ArgumentParser(
241
+ description="Refine MSA by removing sequences with rare indels"
242
+ )
243
+ parser.add_argument("-i", "--input", required=True,
244
+ help="Input aligned FASTA file")
245
+ parser.add_argument("-o", "--output", required=True,
246
+ help="Output prefix for cleaned files")
247
+ parser.add_argument("-f", "--min-freq", type=float, default=0.05,
248
+ help="Minimum indel frequency threshold (default: 0.05 = 5%%)")
249
+ parser.add_argument("-v", "--verbose", action="store_true",
250
+ help="Verbose output")
251
+
252
+ args = parser.parse_args()
253
+
254
+ output_clean = f"{args.output}.clean.fasta"
255
+ output_removed = f"{args.output}.removed.fasta"
256
+ output_stats = f"{args.output}.indel_stats.tsv"
257
+
258
+ stats = refine_msa(
259
+ input_fasta=args.input,
260
+ output_clean=output_clean,
261
+ output_removed=output_removed,
262
+ output_stats=output_stats,
263
+ min_freq=args.min_freq,
264
+ verbose=args.verbose
265
+ )
266
+
267
+ print(f"\nRefinement complete:")
268
+ print(f" Total: {stats['total']} → Clean: {stats['clean']}, Removed: {stats['removed']}")
269
+ print(f" Indels: {stats['total_indels']} total, {stats['rare_indels']} rare (< {stats['min_freq']*100:.1f}%)")
270
+
271
+
272
+ if __name__ == "__main__":
273
+ main()
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ ArraySplitter Refine - Post-processing of decomposed monomers.
5
+
6
+ This module refines the .monomers.tsv file by:
7
+ 1. Splitting merged monomers (doubled, tripled, etc.) that have mutations in cut sites
8
+ 2. Merging fragmented half-monomers with their neighbors
9
+
10
+ Input/Output: .monomers.tsv format
11
+ """
12
+
13
+ import argparse
14
+ from collections import Counter
15
+ from typing import List, Dict, Optional, Tuple
16
+ import editdistance as ed
17
+
18
+
19
+ def read_monomers_tsv(filepath: str) -> Tuple[List[str], List[Dict]]:
20
+ """
21
+ Read .monomers.tsv file.
22
+ Returns (header_fields, list of row dicts).
23
+ """
24
+ rows = []
25
+ header_fields = []
26
+
27
+ with open(filepath, 'r') as f:
28
+ header = f.readline().strip()
29
+ header_fields = header.split('\t')
30
+
31
+ for line in f:
32
+ line = line.strip()
33
+ if not line:
34
+ continue
35
+ parts = line.split('\t')
36
+ row = {}
37
+ for i, field in enumerate(header_fields):
38
+ if i < len(parts):
39
+ row[field] = parts[i]
40
+ else:
41
+ row[field] = ''
42
+
43
+ # Convert numeric fields
44
+ if 'length' in row:
45
+ row['length'] = int(row['length']) if row['length'] else 0
46
+ if 'index' in row:
47
+ row['index'] = int(row['index']) if row['index'] else 0
48
+
49
+ rows.append(row)
50
+
51
+ return header_fields, rows
52
+
53
+
54
+ def write_monomers_tsv(filepath: str, header_fields: List[str], rows: List[Dict]):
55
+ """Write rows to .monomers.tsv file."""
56
+ with open(filepath, 'w') as f:
57
+ f.write('\t'.join(header_fields) + '\n')
58
+ for row in rows:
59
+ values = []
60
+ for field in header_fields:
61
+ val = row.get(field, '')
62
+ values.append(str(val))
63
+ f.write('\t'.join(values) + '\n')
64
+
65
+
66
+ def find_cut_sequence(rows: List[Dict], kmer_len: int = 16) -> Optional[str]:
67
+ """
68
+ Discover the most common starting k-mer from monomers.
69
+ """
70
+ kmer_counts: Counter = Counter()
71
+
72
+ for row in rows:
73
+ if row.get('type') == 'MONOMER':
74
+ seq = row.get('sequence', '')
75
+ if len(seq) >= kmer_len:
76
+ kmer = seq[:kmer_len]
77
+ kmer_counts[kmer] += 1
78
+
79
+ if not kmer_counts:
80
+ return None
81
+
82
+ return kmer_counts.most_common(1)[0][0]
83
+
84
+
85
+ def get_expected_lengths(rows: List[Dict]) -> Tuple[List[int], int]:
86
+ """
87
+ Analyze length distribution to find expected monomer lengths.
88
+ Returns (common_lengths, modal_length).
89
+ """
90
+ lengths = []
91
+ for row in rows:
92
+ if row.get('type') == 'MONOMER':
93
+ lengths.append(row.get('length', 0))
94
+
95
+ if not lengths:
96
+ return [], 700
97
+
98
+ length_counts = Counter(lengths)
99
+ common_lengths = [l for l, c in length_counts.most_common(10) if c >= 5]
100
+
101
+ if not common_lengths:
102
+ common_lengths = [length_counts.most_common(1)[0][0]]
103
+
104
+ modal_length = common_lengths[0]
105
+ return common_lengths, modal_length
106
+
107
+
108
+ def find_approximate_cut_positions(sequence: str, cut_seq: str,
109
+ max_mismatches: int = 3) -> List[int]:
110
+ """
111
+ Find positions where cut_seq appears with up to max_mismatches.
112
+ """
113
+ positions = []
114
+ cut_len = len(cut_seq)
115
+
116
+ for i in range(len(sequence) - cut_len + 1):
117
+ window = sequence[i:i + cut_len]
118
+ dist = ed.eval(cut_seq, window)
119
+ if dist <= max_mismatches:
120
+ positions.append(i)
121
+
122
+ return positions
123
+
124
+
125
+ def split_sequence(sequence: str, cut_seq: str, expected_length: int,
126
+ max_mismatches: int = 3) -> List[str]:
127
+ """
128
+ Split a merged sequence into individual monomers.
129
+ """
130
+ seq_len = len(sequence)
131
+ num_monomers = round(seq_len / expected_length)
132
+
133
+ if num_monomers < 2:
134
+ return [sequence]
135
+
136
+ cut_positions = find_approximate_cut_positions(sequence, cut_seq, max_mismatches)
137
+
138
+ if not cut_positions:
139
+ return [sequence]
140
+
141
+ # Filter positions - need minimum distance between cuts
142
+ min_distance = expected_length * 0.6
143
+ filtered_positions = []
144
+ last_pos = 0
145
+
146
+ for pos in sorted(cut_positions):
147
+ if pos > 0 and pos - last_pos >= min_distance:
148
+ filtered_positions.append(pos)
149
+ last_pos = pos
150
+
151
+ if not filtered_positions:
152
+ return [sequence]
153
+
154
+ # Split at found positions
155
+ result = []
156
+ prev_pos = 0
157
+
158
+ for pos in filtered_positions:
159
+ part = sequence[prev_pos:pos]
160
+ if part:
161
+ result.append(part)
162
+ prev_pos = pos
163
+
164
+ last_part = sequence[prev_pos:]
165
+ if last_part:
166
+ result.append(last_part)
167
+
168
+ return result
169
+
170
+
171
+ def refine_monomers_tsv(input_tsv: str, output_tsv: str,
172
+ expected_length: Optional[int] = None,
173
+ max_mismatches: int = 3,
174
+ min_length_ratio: float = 0.5,
175
+ max_length_ratio: float = 1.5,
176
+ verbose: bool = False) -> Dict:
177
+ """
178
+ Main refinement function for TSV files.
179
+ """
180
+ # Read input
181
+ header_fields, rows = read_monomers_tsv(input_tsv)
182
+
183
+ if verbose:
184
+ print(f"Read {len(rows)} rows from {input_tsv}")
185
+
186
+ # Find cut sequence
187
+ cut_seq = find_cut_sequence(rows)
188
+ if cut_seq is None:
189
+ print("Warning: Could not detect cut sequence")
190
+ if verbose:
191
+ print(f"Detected cut sequence: {cut_seq}")
192
+
193
+ # Analyze length distribution
194
+ common_lengths, modal_length = get_expected_lengths(rows)
195
+
196
+ if expected_length is None:
197
+ expected_length = modal_length
198
+
199
+ if verbose:
200
+ print(f"Expected monomer length: {expected_length} bp")
201
+ print(f"Common lengths: {common_lengths[:5]}")
202
+
203
+ # Define thresholds
204
+ min_length = int(expected_length * min_length_ratio)
205
+ max_length = int(expected_length * max_length_ratio)
206
+
207
+ if verbose:
208
+ print(f"Normal range: {min_length}-{max_length} bp")
209
+
210
+ # Process rows
211
+ refined_rows = []
212
+ stats = {
213
+ 'total_input': len(rows),
214
+ 'normal': 0,
215
+ 'split': 0,
216
+ 'merged': 0,
217
+ 'flanks': 0,
218
+ 'split_into': 0,
219
+ 'merged_from': 0,
220
+ }
221
+
222
+ i = 0
223
+ while i < len(rows):
224
+ row = rows[i]
225
+ row_type = row.get('type', '')
226
+ seq = row.get('sequence', '')
227
+ seq_len = row.get('length', len(seq))
228
+
229
+ # Skip flanks - keep as is
230
+ if 'FLANK' in row_type:
231
+ refined_rows.append(row)
232
+ stats['flanks'] += 1
233
+ i += 1
234
+ continue
235
+
236
+ # Normal length - keep as is
237
+ if min_length <= seq_len <= max_length:
238
+ refined_rows.append(row)
239
+ stats['normal'] += 1
240
+ i += 1
241
+ continue
242
+
243
+ # Merged monomer (too long) - try to split
244
+ if seq_len > max_length:
245
+ if cut_seq is not None:
246
+ parts = split_sequence(seq, cut_seq, expected_length, max_mismatches)
247
+
248
+ if len(parts) > 1:
249
+ stats['split'] += 1
250
+ stats['split_into'] += len(parts)
251
+
252
+ if verbose:
253
+ print(f" Split {seq_len}bp -> {[len(p) for p in parts]}")
254
+
255
+ # Create new rows for each part
256
+ for j, part in enumerate(parts):
257
+ new_row = row.copy()
258
+ new_row['sequence'] = part
259
+ new_row['length'] = len(part)
260
+ new_row['type'] = f"MONOMER|split_{j+1}of{len(parts)}"
261
+ refined_rows.append(new_row)
262
+
263
+ i += 1
264
+ continue
265
+
266
+ # Could not split
267
+ row_copy = row.copy()
268
+ row_copy['type'] = row_type + '|unsplit'
269
+ refined_rows.append(row_copy)
270
+ i += 1
271
+ continue
272
+
273
+ # Short monomer - try to merge with neighbor
274
+ if seq_len < min_length:
275
+ merged = False
276
+
277
+ # Look at next row
278
+ if i + 1 < len(rows):
279
+ next_row = rows[i + 1]
280
+ next_type = next_row.get('type', '')
281
+ next_seq = next_row.get('sequence', '')
282
+ next_len = next_row.get('length', len(next_seq))
283
+
284
+ # Check if same array and both are short monomers
285
+ if (row.get('sequence_id') == next_row.get('sequence_id') and
286
+ 'FLANK' not in next_type and
287
+ next_len < min_length):
288
+
289
+ combined_len = seq_len + next_len
290
+ # Check if combined is reasonable
291
+ if min_length <= combined_len <= max_length * 1.1:
292
+ # Merge
293
+ merged_seq = seq + next_seq
294
+ merged_row = row.copy()
295
+ merged_row['sequence'] = merged_seq
296
+ merged_row['length'] = len(merged_seq)
297
+ merged_row['type'] = 'MONOMER|merged'
298
+ refined_rows.append(merged_row)
299
+
300
+ stats['merged'] += 1
301
+ stats['merged_from'] += 2
302
+
303
+ if verbose:
304
+ print(f" Merged {seq_len}bp + {next_len}bp = {len(merged_seq)}bp")
305
+
306
+ i += 2
307
+ merged = True
308
+
309
+ if not merged:
310
+ # Keep as is with tag
311
+ row_copy = row.copy()
312
+ row_copy['type'] = row_type + '|short'
313
+ refined_rows.append(row_copy)
314
+ i += 1
315
+ continue
316
+
317
+ # Fallback
318
+ refined_rows.append(row)
319
+ i += 1
320
+
321
+ stats['total_output'] = len(refined_rows)
322
+
323
+ # Renumber indices within each sequence_id
324
+ current_seq_id = None
325
+ current_idx = 0
326
+ for row in refined_rows:
327
+ seq_id = row.get('sequence_id')
328
+ if seq_id != current_seq_id:
329
+ current_seq_id = seq_id
330
+ current_idx = 0
331
+ row['index'] = current_idx
332
+ current_idx += 1
333
+
334
+ # Write output
335
+ write_monomers_tsv(output_tsv, header_fields, refined_rows)
336
+
337
+ if verbose:
338
+ print(f"\nRefinement statistics:")
339
+ print(f" Input rows: {stats['total_input']}")
340
+ print(f" Output rows: {stats['total_output']}")
341
+ print(f" Normal (unchanged): {stats['normal']}")
342
+ print(f" Split merged: {stats['split']} -> {stats['split_into']} parts")
343
+ print(f" Merged short: {stats['merged']} (from {stats['merged_from']} fragments)")
344
+ print(f" Flanks (unchanged): {stats['flanks']}")
345
+
346
+ return stats
347
+
348
+
349
+ def main(input_tsv: str, output_tsv: str,
350
+ expected_length: Optional[int] = None,
351
+ max_mismatches: int = 3,
352
+ verbose: bool = False) -> Dict:
353
+ """Main entry point for refine command."""
354
+
355
+ stats = refine_monomers_tsv(
356
+ input_tsv=input_tsv,
357
+ output_tsv=output_tsv,
358
+ expected_length=expected_length,
359
+ max_mismatches=max_mismatches,
360
+ verbose=verbose
361
+ )
362
+
363
+ return stats
364
+
365
+
366
+ if __name__ == "__main__":
367
+ parser = argparse.ArgumentParser(
368
+ description="Refine ArraySplitter .monomers.tsv by splitting merged monomers and joining fragments"
369
+ )
370
+ parser.add_argument("-i", "--input", required=True,
371
+ help="Input .monomers.tsv file")
372
+ parser.add_argument("-o", "--output", required=True,
373
+ help="Output .monomers.tsv file (refined)")
374
+ parser.add_argument("-l", "--length", type=int, default=None,
375
+ help="Expected monomer length (auto-detected if not specified)")
376
+ parser.add_argument("-m", "--mismatches", type=int, default=3,
377
+ help="Maximum mismatches for cut sequence matching (default: 3)")
378
+ parser.add_argument("-v", "--verbose", action="store_true",
379
+ help="Verbose output")
380
+
381
+ args = parser.parse_args()
382
+
383
+ main(
384
+ input_tsv=args.input,
385
+ output_tsv=args.output,
386
+ expected_length=args.length,
387
+ max_mismatches=args.mismatches,
388
+ verbose=args.verbose
389
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ArraySplitter
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: De Novo Decomposition of Satellite DNA Arrays into Monomers within Telomere-to-Telomere Assemblies
5
5
  Home-page: https://github.com/aglabx/ArraySplitter
6
6
  Author: Aleksey Komissarov
@@ -1,8 +1,12 @@
1
- ArraySplitter/__init__.py,sha256=h8Fz1eD4MXGpCgKpn1RqfA_t7dfCg6O8Vx0ysxCMdHE,169
1
+ ArraySplitter/__init__.py,sha256=FzGKjGO2V7GyNG6KBBUhvlc9IeGFcEMJofm6vKxgGEg,169
2
2
  ArraySplitter/classify.py,sha256=9OkmGecGvEa40mmIK4BPblVpuRBfZFS7oWbkETJ2idQ,21023
3
3
  ArraySplitter/decompose.py,sha256=Voqcwgquy6-lSnVjusjPrKiszxG5x0SIhmjrtGCTVvo,70530
4
4
  ArraySplitter/extract.py,sha256=bqlC8E6MjndMCBa44Zvv23gXmH2yr7IdQBXnc5foqhM,2300
5
- ArraySplitter/main.py,sha256=GTOI6Ca38-Ws_a127MbLtPnSzKPBKrDFHS48_1Tey54,5709
5
+ ArraySplitter/indel_chromosome_analysis.py,sha256=xSgJqY1Pmeie47hnuc2tKZvyuB0RWpfTAwYB_K3cJ5w,8283
6
+ ArraySplitter/main.py,sha256=hoFw4OMMF27nmz0eyg1-gMtpQ43sXBkqQAyads5Lmrw,8799
7
+ ArraySplitter/monomer_rotation.py,sha256=tdpqmNJYJQxatsEZ6v6ieEUizZQTS66ggrDKosQk1FM,8023
8
+ ArraySplitter/msa_refine.py,sha256=AHbUzRv7uCpxCJ9AFAWDEEwahz57wRiYT3XaFu9cpuw,8886
9
+ ArraySplitter/refine.py,sha256=CmXuUViCbbGo5ZtojfAzqyVaV2DA7RlIITFMoKN8ou4,12069
6
10
  ArraySplitter/rotate.py,sha256=ZvH2WDnRlfeqbXtUDmrgqE2CQEGe5qKDVR-my_uKkLA,1231
7
11
  ArraySplitter/core_functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
12
  ArraySplitter/core_functions/io/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -19,9 +23,9 @@ ArraySplitter/core_functions/tools/fs_tree.py,sha256=OHvamldtHW_7VEiWg7XiJ7Xwml2
19
23
  ArraySplitter/core_functions/tools/parsers.py,sha256=J_rtt384ycfgrWlgrazb3qj7Sr0waWj7yShlril5rqM,6766
20
24
  ArraySplitter/core_functions/tools/rotation.py,sha256=v_-3gkx8AR10RDCNMO82Pok1ZbqT2Zy9asyWZ6Ib_v8,3109
21
25
  ArraySplitter/core_functions/tools/sequences.py,sha256=v74uHp1nTF_UvMRefYmJfMbnzdurRn995C06hugoSa4,441
22
- arraysplitter-1.3.0.dist-info/licenses/LICENSE,sha256=m2dY1BxIk3gKB3jSa8kDtlSnNfW68VumXAQzGacCtvc,1063
23
- arraysplitter-1.3.0.dist-info/METADATA,sha256=E-rQhLl9Cn-xcF9hYuuhjyXxRURf2ca8trtgd8cAa_E,10177
24
- arraysplitter-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
25
- arraysplitter-1.3.0.dist-info/entry_points.txt,sha256=H8EDFoNSqnM6J8m2w97H8HKjzAqs-iNTrIHk87-kfhY,60
26
- arraysplitter-1.3.0.dist-info/top_level.txt,sha256=zk1yzRnjygCl8ZF8rx3w4WSWdW8FUw1-MqSk-s3NOHY,14
27
- arraysplitter-1.3.0.dist-info/RECORD,,
26
+ arraysplitter-1.4.0.dist-info/licenses/LICENSE,sha256=m2dY1BxIk3gKB3jSa8kDtlSnNfW68VumXAQzGacCtvc,1063
27
+ arraysplitter-1.4.0.dist-info/METADATA,sha256=tyZZZNL2E6gqOWVVlmAEbevdHD2QmX8O_-ropuYoWvQ,10177
28
+ arraysplitter-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ arraysplitter-1.4.0.dist-info/entry_points.txt,sha256=H8EDFoNSqnM6J8m2w97H8HKjzAqs-iNTrIHk87-kfhY,60
30
+ arraysplitter-1.4.0.dist-info/top_level.txt,sha256=zk1yzRnjygCl8ZF8rx3w4WSWdW8FUw1-MqSk-s3NOHY,14
31
+ arraysplitter-1.4.0.dist-info/RECORD,,