<commit_before>#!/usr/bin/env python3

import os
import sys
import tempfile
import shutil
import argparse
import copy
import pyfastaq
import pymummer


# coords = list of tuples [(x1, y1), (x2, y2) ...]
def svg_polygon(coords, fill_colour, border_colour, border_width = 1, opacity=-1):
    return_string = '<polygon points="' + ' '.join([str(x[0])+','+str(x[1]) for x in coords]) + '" ' \
        + 'fill="' + fill_colour + '" '

    if opacity != -1:
        return_string += 'fill-opacity="' + str(opacity) + '" '

    return_string += 'stroke="' + border_colour + '" ' \
                     + 'stroke-width="' + str(border_width) + '" ' \
                     + '/>'
    return return_string


class Appearance:
    def __init__(self, opts):
        self.contig_fill_colour = opts.seq_col
        self.contig_border_colour = opts.seq_col
        self.contig_border_width = 0
        self.contig_x_space = opts.contig_x_space
        self.match_fwd_fill_colour = opts.fwd_match_col
        self.match_fwd_border_colour = opts.fwd_match_col
        self.match_fwd_border_width = 0
        self.match_rev_fill_colour = opts.rev_match_col
        self.match_rev_border_colour = opts.rev_match_col
        self.match_rev_border_width = 0
        self.match_opacity = opts.match_opacity
        self.match_min_length_bases = 10000
        self.match_min_length_ratio = 0.2


class Assembly:
    def __init__(self, fasta_file):
        self.names = []
        self.lengths = {}
        seq_reader = pyfastaq.sequences.file_reader(fasta_file)
        for seq in seq_reader:
            self.lengths[seq.id] = len(seq)
            self.names.append(seq.id)


    def x_width(self, contig_x_space):
        return sum(self.lengths.values()) + contig_x_space * (len(self.lengths) - 1)


    def contigs_svg(self, scale_factor, y_top, y_bottom, appearance):
        x_in_bases = 0
        lines = []
        self.contig_coords = {}

        for contig in self.names:
            x_end = x_in_bases + self.lengths[contig]
            x_left = x_in_bases * scale_factor
            x_right = x_end * scale_factor
            self.contig_coords[contig] = (x_left, x_right)
            coords = [(x_left, y_top), (x_right, y_top), (x_right, y_bottom), (x_left, y_bottom)]
            lines.append(
                svg_polygon(
                    coords,
                    appearance.contig_fill_colour,
                    appearance.contig_border_colour,
                    border_width=appearance.contig_border_width
                )
            )
            x_in_bases = x_end + appearance.contig_x_space

        return '\n'.join(lines)


class Assemblies:
    def __init__(self, fasta_files, appearance, outprefix, nucmer_min_id=98, nucmer_min_length=250, simplify=True):
        self.fasta_files = fasta_files
        self.appearance = appearance
        self.outprefix = outprefix
        ok = True

        for filename in fasta_files:
            if not os.path.exists(filename):
                print('Could not find file:', filename, file=sys.stderr)
                ok = False

        if not ok:
            sys.exit(1)

        self.nucmer_min_id = nucmer_min_id
        self.nucmer_min_length = nucmer_min_length
        self.simplify = simplify
        self.assemblies = {filename: Assembly(filename) for filename in fasta_files}


    @staticmethod
    def _run_nucmer(ref, qry, outfile, min_id=98, simplify=True, min_length=250):
        n = pymummer.nucmer.Runner(
            ref,
            qry,
            outfile,
            min_id=min_id,
            min_length=250,
            breaklen=500,
            maxmatch=False,
            simplify=simplify,
            verbose=True,
        )
        n.run()


    @staticmethod
    def _get_x_max(assemblies, contig_x_space):
        return max([a.x_width(contig_x_space) for a in assemblies.values()])


    def _make_nucmer_files(self, outprefix):
        self.nucmer_matches = []
        self.nucmer_files = []

        for i in range(len(self.assemblies) - 1):
            print('Comparing assembly ', i, ' (', self.fasta_files[i], ') against ', i+1, ' (', self.fasta_files[i+1], ')', sep='')
            nucmer_file = '.'.join([outprefix, str(i), str(i+1), 'coords'])
            if os.path.exists(nucmer_file):
                print('Found nucmer coords file', nucmer_file, 'so no need to run nucmer')
            else:
                print('Running nucmer. Coords file will be called:', nucmer_file)
                self._run_nucmer(
                    self.fasta_files[i+1],
                    self.fasta_files[i],
                    nucmer_file,
                    min_id=self.nucmer_min_id,
                    simplify=self.simplify,
                    min_length=self.nucmer_min_length
                )

            self.nucmer_matches.append([x for x in pymummer.coords_file.reader(nucmer_file)])


    @staticmethod
    def _write_svg_header(filehandle, width, height):
        print(r'''<?xml version="1.0" standalone="no"?>''', file=filehandle)
        print(r'''<!DOCTYPE svg PUBLIC " -//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">''', file=filehandle)
        print(r'<svg width="' + str(width) + '" height="' + str(height) + '">', file=filehandle)



    def _write_svg_contigs(self, filehandle):
        y_top = 0
        for filename in self.fasta_files:
            y_bottom = y_top + self.contig_height
            print(self.assemblies[filename].contigs_svg(self.x_scale_factor, y_top, y_bottom, self.appearance), file=filehandle)
            y_top += self.contig_height + 2 * self.y_space + self.match_height


    def _write_svg_matches_between_two_assemblies(self, nucmer_matches, top_assembly, bottom_assembly, y_top):
        # top assembly = the query in nucmer matches
        # bottom assembly = the reference in nucmer matches
        lines = []
        y_bottom = y_top + self.match_height
        for match in nucmer_matches:
            if match.hit_length_qry < min(self.appearance.match_min_length_bases, self.appearance.match_min_length_ratio * min(match.qry_length, match.ref_length)):<commit_msg>No min length for nucmer run<commit_after>#!/usr/bin/env python3

import os
import sys
import tempfile
import shutil
import argparse
import copy
import pyfastaq
import pymummer


# coords = list of tuples [(x1, y1), (x2, y2) ...]
def svg_polygon(coords, fill_colour, border_colour, border_width = 1, opacity=-1):
    return_string = '<polygon points="' + ' '.join([str(x[0])+','+str(x[1]) for x in coords]) + '" ' \
        + 'fill="' + fill_colour + '" '

    if opacity != -1:
        return_string += 'fill-opacity="' + str(opacity) + '" '

    return_string += 'stroke="' + border_colour + '" ' \
                     + 'stroke-width="' + str(border_width) + '" ' \
                     + '/>'
    return return_string


class Appearance:
    def __init__(self, opts):
        self.contig_fill_colour = opts.seq_col
        self.contig_border_colour = opts.seq_col
        self.contig_border_width = 0
        self.contig_x_space = opts.contig_x_space
        self.match_fwd_fill_colour = opts.fwd_match_col
        self.match_fwd_border_colour = opts.fwd_match_col
        self.match_fwd_border_width = 0
        self.match_rev_fill_colour = opts.rev_match_col
        self.match_rev_border_colour = opts.rev_match_col
        self.match_rev_border_width = 0
        self.match_opacity = opts.match_opacity
        self.match_min_length_bases = 10000
        self.match_min_length_ratio = 0.2


class Assembly:
    def __init__(self, fasta_file):
        self.names = []
        self.lengths = {}
        seq_reader = pyfastaq.sequences.file_reader(fasta_file)
        for seq in seq_reader:
            self.lengths[seq.id] = len(seq)
            self.names.append(seq.id)


    def x_width(self, contig_x_space):
        return sum(self.lengths.values()) + contig_x_space * (len(self.lengths) - 1)


    def contigs_svg(self, scale_factor, y_top, y_bottom, appearance):
        x_in_bases = 0
        lines = []
        self.contig_coords = {}

        for contig in self.names:
            x_end = x_in_bases + self.lengths[contig]
            x_left = x_in_bases * scale_factor
            x_right = x_end * scale_factor
            self.contig_coords[contig] = (x_left, x_right)
            coords = [(x_left, y_top), (x_right, y_top), (x_right, y_bottom), (x_left, y_bottom)]
            lines.append(
                svg_polygon(
                    coords,
                    appearance.contig_fill_colour,
                    appearance.contig_border_colour,
                    border_width=appearance.contig_border_width
                )
            )
            x_in_bases = x_end + appearance.contig_x_space

        return '\n'.join(lines)


class Assemblies:
    def __init__(self, fasta_files, appearance, outprefix, nucmer_min_id=98, nucmer_min_length=250, simplify=True):
        self.fasta_files = fasta_files
        self.appearance = appearance
        self.outprefix = outprefix
        ok = True

        for filename in fasta_files:
            if not os.path.exists(filename):
                print('Could not find file:', filename, file=sys.stderr)
                ok = False

        if not ok:
            sys.exit(1)

        self.nucmer_min_id = nucmer_min_id
        self.nucmer_min_length = nucmer_min_length
        self.simplify = simplify
        self.assemblies = {filename: Assembly(filename) for filename in fasta_files}


    @staticmethod
    def _get_x_max(assemblies, contig_x_space):
        return max([a.x_width(contig_x_space) for a in assemblies.values()])


    def _make_nucmer_files(self, outprefix):
        self.nucmer_matches = []
        self.nucmer_files = []

        for i in range(len(self.assemblies) - 1):
            print('Comparing assembly ', i, ' (', self.fasta_files[i], ') against ', i+1, ' (', self.fasta_files[i+1], ')', sep='')
            nucmer_file = '.'.join([outprefix, str(i), str(i+1), 'coords'])
            if os.path.exists(nucmer_file):
                print('Found nucmer coords file', nucmer_file, 'so no need to run nucmer')
            else:
                print('Running nucmer. Coords file will be called:', nucmer_file)
                n = pymummer.nucmer.Runner(
                    self.fasta_files[i+1],
                    self.fasta_files[i],
                    nucmer_file,
                    min_id=self.nucmer_min_id,
                    breaklen=500,
                    maxmatch=False,
                    simplify=True,
                    verbose=False,
                )
                n.run()

            self.nucmer_matches.append([x for x in pymummer.coords_file.reader(nucmer_file)])


    @staticmethod
    def _write_svg_header(filehandle, width, height):
        print(r'''<?xml version="1.0" standalone="no"?>''', file=filehandle)
        print(r'''<!DOCTYPE svg PUBLIC " -//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">''', file=filehandle)
        print(r'<svg width="' + str(width) + '" height="' + str(height) + '">', file=filehandle)



    def _write_svg_contigs(self, filehandle):
        y_top = 0
        for filename in self.fasta_files:
            y_bottom = y_top + self.contig_height
            print(self.assemblies[filename].contigs_svg(self.x_scale_factor, y_top, y_bottom, self.appearance), file=filehandle)
            y_top += self.contig_height + 2 * self.y_space + self.match_height


    def _write_svg_matches_between_two_assemblies(self, nucmer_matches, top_assembly, bottom_assembly, y_top):
        # top assembly = the query in nucmer matches
        # bottom assembly = the reference in nucmer matches
        lines = []
        y_bottom = y_top + self.match_height
        for match in nucmer_matches:
            if match.hit_length_qry < min(self.appearance.match_min_length_bases, self.appearance.match_min_length_ratio * min(match.qry_length, match.ref_length)):
{'fraction_non_alphanumeric': 0.1095468177274242, 'fraction_numerical': 0.010329890036654448, 'mean_word_length': 2.269335511982571, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 2, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '745964', 'n_tokens_mistral': 4108, 'n_tokens_neox': 3881, 'n_words': 734}