view SMART/Java/Python/plotCoverage.py @ 44:5f796c5c579f

Uploaded
author m-zytnicki
date Wed, 18 Sep 2013 08:32:38 -0400
parents 44d5973c188c
children 169d364ddd91
line wrap: on
line source

#! /usr/bin/env python
#
# Copyright INRA-URGI 2009-2010
# 
# This software is governed by the CeCILL license under French law and
# abiding by the rules of distribution of free software. You can use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
# 
# As a counterpart to the access to the source code and rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty and the software's author, the holder of the
# economic rights, and the successive licensors have only limited
# liability.
# 
# In this respect, the user's attention is drawn to the risks associated
# with loading, using, modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean that it is complicated to manipulate, and that also
# therefore means that it is reserved for developers and experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and, more generally, to use and operate it in the
# same conditions as regards security.
# 
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.
#
import os, os.path, subprocess, glob, random
from optparse import OptionParser
from SMART.Java.Python.structure.Interval import Interval
from SMART.Java.Python.structure.Transcript import Transcript
from commons.core.parsing.ParserChooser import ParserChooser
from SMART.Java.Python.misc.RPlotter import RPlotter
from SMART.Java.Python.misc.Progress import Progress
from commons.core.parsing.FastaParser import FastaParser

strands = [-1, 1]
colors  = {-1: "blue", 1: "red", 0: "black"}
colorLine = "black"

def parseTargetField(field):
	strand             = "+"
	splittedFieldSpace = field.split()
	splittedFieldPlus  = field.split("+", 4)
	if len(splittedFieldSpace) == 3:
		id, start, end = splittedFieldSpace
	elif len(splittedFieldSpace) == 4:
		id, start, end, strand = splittedFieldSpace
	elif len(splittedFieldPlus) == 3:
		id, start, end = splittedFieldPlus
	elif len(splittedFieldPlus) == 4:
		id, start, end, strand = splittedFieldPlus
	else:
		raise Exception("Cannot parse Target field '%s'." % (field))
	return (id, int(start), int(end), strand)


class SimpleTranscript(object):
	def __init__(self, transcript1, transcript2, color = None):
		self.start  = max(0, transcript1.getStart() - transcript2.getStart())
		self.end    = min(transcript2.getEnd() - transcript2.getStart(), transcript1.getEnd() - transcript2.getStart())
		self.strand = transcript1.getDirection() * transcript2.getDirection()
		self.exons  = []
		for exon in transcript1.getExons():
			if exon.getEnd() >= transcript2.getStart() and exon.getStart() <= transcript2.getEnd():
				start = max(0, exon.getStart() - transcript2.getStart())
				end   = min(transcript2.getEnd() - transcript2.getStart(), exon.getEnd() - transcript2.getStart())
				self.addExon(start, end, self.strand, color)

	def addExon(self, start, end, strand, color):
		exon = SimpleExon(start, end, strand, color)
		self.exons.append(exon)

	def getRScript(self, yOffset, height):
		rString     = ""
		previousEnd = None
		for exon in sorted(self.exons, key=lambda exon: exon.start):
			if previousEnd != None:
				rString += "segments(%.1f, %.1f, %.1f, %.1f, col = \"%s\")\n" % (previousEnd, yOffset + height / 4.0, exon.start, yOffset + height / 4.0, colorLine)
			rString    += exon.getRScript(yOffset, height)
			previousEnd = exon.end
		return rString


class SimpleExon(object):
	def __init__(self, start, end, strand, color = None):
		self.start  = start
		self.end    = end
		self.strand = strand
		self.color  = color
		
	def getRScript(self, yOffset, height):
		color = self.color if self.color != None else colors[self.strand]
		return "rect(%.1f, %.1f, %.1f, %.1f, col=\"%s\", border = \"%s\")\n" % (self.start, yOffset, self.end, yOffset + height / 2.0, color, colorLine)


class Plotter(object):
	
	def __init__(self, seed, index, verbosity):
		self.seed        = seed
		self.index       = index
		self.verbosity   = verbosity
		self.maxCoverage = 0
		self.maxOverlap  = 0
		self.log         = ""
		self.merge       = False
		self.width       = 1500
		self.heigth      = 1000
		self.xLabel      = ""
		self.yLabel      = ""
		self.title       = None
		self.absPath     = os.getcwd()
		self.coverageDataFileName    = "tmpFile_%d_%s.dat" % (seed, index)
		self.coverageScript          = ""
		self.overlapScript           = ""
		self.outputFileName          = None

	def setOutputFileName(self, fileName):
		self.outputFileName = fileName

	def setTranscript(self, transcript):
		self.transcript = transcript
		self.name       = transcript.getName()
		self.size       = transcript.getEnd() - transcript.getStart() + 1
		if self.title == None:
			self.title = self.name
		else:
			self.title += " " + self.name

	def setTitle(self, title):
		self.title = title + " " + self.name

	def setPlotSize(self, width, height):
		self.width  = width
		self.height = height

	def setLabels(self, xLabel, yLabel):
		self.xLabel = xLabel
		self.yLabel = yLabel

	def setMerge(self, merge):
		self.merge = merge

	def setCoverageData(self, coverage):
		outputCoveragePerStrand = dict([strand, 0] for strand in strands)
		outputCoverage          = 0
		dataFile = open(os.path.abspath(self.coverageDataFileName), "w")
		for position in range(self.size+1):
			sumValue = 0
			found    = False
			dataFile.write("%d\t" % (position))
			for strand in strands:
				value     = coverage[strand].get(position, 0)
				sumValue += value
				dataFile.write("%d\t" % (value))
				if value > 0:
					found = True
					outputCoveragePerStrand[strand] += 1
			self.maxCoverage = max(self.maxCoverage, sumValue)
			dataFile.write("%d\n" % (sumValue))
			if found:
				outputCoverage += 1
		dataFile.close()
		self.log += "%s (%d nt):\n - both strands: %d (%.0f%%)\n - (+) strand: %d (%.0f%%)\n - (-) strand: %d (%.0f%%)\n" % (self.name, self.size, outputCoverage, float(outputCoverage) / self.size * 100, outputCoveragePerStrand[1], float(outputCoveragePerStrand[1]) / self.size * 100, outputCoveragePerStrand[-1], float(outputCoveragePerStrand[-1]) / self.size * 100) 
		self.coverageScript += "data = scan(\"%s\", list(pos = -666, minus = -666, plus = -666, sumValue = -666), sep=\"\t\")\n" % (os.path.abspath(self.coverageDataFileName))
		self.coverageScript += "lines(x = data$pos, y = data$minus,    col = \"%s\")\n" % (colors[-1])
		self.coverageScript += "lines(x = data$pos, y = data$plus,     col = \"%s\")\n" % (colors[1])
		self.coverageScript += "lines(x = data$pos, y = data$sumValue, col = \"%s\")\n" % (colors[0])

	def setOverlapData(self, overlap):
		height              = 1
		self.maxOverlap     = (len(overlap) + 1) * height
		thisElement         = SimpleTranscript(self.transcript, self.transcript, "black")
		self.overlapScript += thisElement.getRScript(0, height)
		for cpt, transcript in enumerate(sorted(overlap, cmp=lambda c1, c2: c1.start - c2.start if c1.start != c2.start else c1.end - c2.end)):
			self.overlapScript += transcript.getRScript((cpt + 1) * height, height)

	def getFirstLine(self, suffix = None):
		return "png(file = \"%s_%s%s.png\", width = %d, height = %d, bg = \"white\")\n" % (self.outputFileName, self.name, "" if suffix == None or self.merge else "_%s" % (suffix), self.width, self.height)

	def getLastLine(self):
		return "dev.off()\n"

	def startR(self, fileName, script):
		scriptFile = open(fileName, "w")
		scriptFile.write(script)
		scriptFile.close()
		command = "R CMD BATCH %s" % (fileName)
		status  = subprocess.call(command, shell=True)
		if status != 0:
			raise Exception("Problem with the execution of script file %s, status is: %s" % (fileName, status))

	def plot(self):
		if self.merge:
			fileName = "%s_%d_%s.R" % (self.outputFileName, self.seed, self.index)
			plotLine = "plot(x = NA, y = NA, xlab=\"%s\", ylab=\"%s\", panel.first = grid(lwd = 1.0), xlim = c(0, %d), ylim = c(0, %d), cex.axis = 2, cex.lab = 2, cex.main=2, main = \"%s\")\n" % (self.xLabel, self.yLabel, self.size, max(self.maxCoverage, self.maxOverlap), self.title)
			script   = self.getFirstLine() + plotLine + self.overlapScript + self.coverageScript + self.getLastLine()
			self.startR(fileName, script)
		else:
			fileName = "%s_%d_%s_overlap.R" % (self.outputFileName, self.seed, self.index)
			plotLine = "plot(x = NA, y = NA, xlab=\"%s\", ylab=\"%s\", panel.first = grid(lwd = 1.0), xlim = c(0, %d), ylim = c(0, %d), cex.axis = 2, cex.lab = 2, cex.main=2, main = \"%s\")\n" % (self.xLabel, self.yLabel, self.size, self.maxOverlap, self.title)
			script   = self.getFirstLine("overlap") + plotLine + self.overlapScript + self.getLastLine()
			self.startR(fileName, script)
			fileName = "%s_%d_%s_coverage.R" % (self.outputFileName, self.seed, self.index)
			plotLine = "plot(x = NA, y = NA, xlab=\"%s\", ylab=\"%s\", panel.first = grid(lwd = 1.0), xlim = c(0, %d), ylim = c(0, %d), cex.axis = 2, cex.lab = 2, cex.main=2, main = \"%s\")\n" % (self.xLabel, self.yLabel, self.size, self.maxCoverage, self.title)
			script   = self.getFirstLine("coverage") + plotLine + self.coverageScript + self.getLastLine()
			self.startR(fileName, script)


class PlotParser(object):

	def __init__(self, verbosity):
		self.verbosity      = verbosity
		self.parsers        = [None, None]
		self.sequenceParser = None
		self.seed           = random.randint(0, 10000)
		self.title          = ""
		self.merge          = False

	def __del__(self):
		for fileName in glob.glob("tmpFile_%d*.dat" % (self.seed)):
			os.remove(fileName)
		for fileName in glob.glob("%s*.R" % (os.path.abspath(self.outputFileName))):
			os.remove(fileName)
		for fileName in glob.glob("%s*.Rout" % (os.path.abspath(self.outputFileName))):
			os.remove(fileName)

	def addInput(self, inputNb, fileName, fileFormat):
		if fileName == None:
			return
		chooser = ParserChooser(self.verbosity)
		chooser.findFormat(fileFormat)
		self.parsers[inputNb] = chooser.getParser(fileName)
		if inputNb == 0:
			self.parsers[1] = self.parsers[0]

	def addSequence(self, fileName):
		if fileName == None:
			return
		self.sequenceParser = FastaParser(fileName, self.verbosity)

	def setOutput(self, fileName):
		self.outputFileName = fileName

	def setPlotSize(self, width, height):
		self.width  = width
		self.height = height

	def setLabels(self, xLabel, yLabel):
		self.xLabel = xLabel
		self.yLabel = yLabel

	def setTitle(self, title):
		self.title = title

	def setMerge(self, merge):
		self.merge = merge

	def initializeDataFromSequences(self):
		self.sizes    = {}
		self.coverage = {}
		self.overlap  = {}
		for region in self.sequenceParser.getRegions():
			self.sizes[region]    = self.sequenceParser.getSizeOfRegion(region)
			self.coverage[region] = {}
			self.overlap[region]  = []
			for strand in strands:
				self.coverage[region][strand] = {}
				self.coverage[region][strand][1] = 0
				self.coverage[region][strand][self.sizes[region]] = 0

	def initializeDataFromTranscripts(self):
		self.coverage = dict([i, None] for i in range(self.parsers[1].getNbTranscripts()))
		self.overlap  = dict([i, None] for i in range(self.parsers[1].getNbTranscripts()))
		self.sizes    = dict([i, 0]    for i in range(self.parsers[1].getNbTranscripts()))
		progress = Progress(self.parsers[1].getNbTranscripts(), "Reading regions", self.verbosity)
		for cpt, transcript in enumerate(self.parsers[1].getIterator()):
			self.coverage[cpt] = {}
			self.overlap[cpt]  = []
			for strand in strands:
				self.coverage[cpt][strand] = {}
				self.coverage[cpt][strand][0] = 0
				self.coverage[cpt][strand][transcript.getEnd() - transcript.getStart()] = 0
			for exon in transcript.getExons():
				self.sizes[cpt] += exon.getSize()
			progress.inc()
		progress.done()

	def initialize(self):
		if self.sequenceParser == None:
			self.initializeDataFromTranscripts()
		else:
			self.initializeDataFromSequences()

	def computeCoverage(self, transcript1, transcript2, id):
		strand = transcript1.getDirection() * transcript2.getDirection()
		for exon1 in transcript1.getExons():
			for exon2 in transcript2.getExons():
				if exon1.overlapWith(exon2):
					for position in range(max(exon1.getStart(), exon2.getStart()), min(exon1.getEnd(), exon2.getEnd()) + 1):
						relativePosition = position - transcript2.getStart() + 1
						self.coverage[id][strand][relativePosition] = self.coverage[id][strand].get(relativePosition, 0) + 1

	def computeOverlap(self, transcript1, transcript2, id):
		simpleTranscript = SimpleTranscript(transcript1, transcript2)
		self.overlap[id].append(simpleTranscript)
		
	def compute2TranscriptFiles(self):
		progress = Progress(self.parsers[1].getNbTranscripts(), "Comparing regions", self.verbosity)
		for cpt2, transcript2 in enumerate(self.parsers[1].getIterator()):
			for transcript1 in self.parsers[0].getIterator():
				if transcript1.overlapWithExon(transcript2):
					self.computeCoverage(transcript1, transcript2, cpt2)
					self.computeOverlap(transcript1, transcript2, cpt2)
			progress.inc()
		progress.done()

	def extractReferenceQueryMapping(self, mapping):
		queryTranscript = mapping.getTranscript()
		referenceTranscript = Transcript()
		referenceTranscript.setChromosome(queryTranscript.getChromosome())
		referenceTranscript.setName(queryTranscript.getChromosome())
		referenceTranscript.setDirection("+")
		referenceTranscript.setEnd(self.sizes[queryTranscript.getChromosome()])
		referenceTranscript.setStart(1)
		return (referenceTranscript, queryTranscript)

	def extractReferenceQuery(self, inputTranscript):
		if "Target" not in inputTranscript.getTagNames():
			raise Exception("Cannot extract Target field in line '%s'." % (inputTranscript))
		id, start, end, strand = parseTargetField(inputTranscript.getTagValue("Target"))
		if id not in self.sizes:
			raise Exception("Target id '%s' of transcript '%s' does not correspond to anything in FASTA file." % (id, inputTranscript))
		referenceTranscript = Transcript()
		referenceTranscript.setChromosome(id)
		referenceTranscript.setName(id)
		referenceTranscript.setDirection("+")
		referenceTranscript.setEnd(self.sizes[id])
		referenceTranscript.setStart(1)
		queryTranscript = Transcript()
		queryTranscript.setChromosome(id)
		queryTranscript.setName(id)
		queryTranscript.setStart(start)
		queryTranscript.setEnd(end)
		queryTranscript.setDirection(strand)
		if inputTranscript.getNbExons() > 1:
			factor = float(end - start) / (inputTranscript.getEnd() - inputTranscript.getStart())
			for exon in inputTranscript.getExons():
				newExon = Interval()
				newExon.setChromosome(id)
				newExon.setDirection(strand)
				if "Target" in inputTranscript.getTagNames():
					id, start, end, strand = parseTargetField(exon.getTagValue("Target"))
					newExon.setStart(start)
					newExon.setEnd(end)
				else:
					newExon.setStart(int(round((exon.getStart() - inputTranscript.getStart()) * factor)) + start)
					newExon.setEnd(  int(round((exon.getEnd() -   inputTranscript.getStart()) * factor)) + start)
				queryTranscript.addExon(newExon)
		return (referenceTranscript, queryTranscript)

	def compute1TranscriptFiles(self):
		progress = Progress(self.parsers[1].getNbItems(), "Comparing regions", self.verbosity)
		for transcript in self.parsers[1].getIterator():
			if transcript.__class__.__name__ == "Mapping":
				referenceTranscript, queryTranscript = self.extractReferenceQueryMapping(transcript)
			else:
				referenceTranscript, queryTranscript = self.extractReferenceQuery(transcript)
			self.computeCoverage(queryTranscript, referenceTranscript, referenceTranscript.getName())
			self.computeOverlap(queryTranscript, referenceTranscript, referenceTranscript.getName())
			progress.inc()
		progress.done()

	def compute(self):
		if self.sequenceParser == None:
			self.compute2TranscriptFiles()
		else:
			self.compute1TranscriptFiles()

	def plotTranscript(self, index, transcript):
		plotter = Plotter(self.seed, index, self.verbosity)
		plotter.setOutputFileName(self.outputFileName)
		plotter.setTranscript(transcript)
		plotter.setTitle(self.title)
		plotter.setLabels(self.xLabel, self.yLabel)
		plotter.setPlotSize(self.width, self.height)
		plotter.setCoverageData(self.coverage[index])
		plotter.setOverlapData(self.overlap[index])
		plotter.setMerge(self.merge)
		plotter.plot()
		output = plotter.log
		return output
		
	def plot1TranscriptFile(self):
		self.outputCoverage          = {}
		self.outputCoveragePerStrand = {}
		output   = ""
		progress = Progress(len(self.sequenceParser.getRegions()), "Plotting regions", self.verbosity)
		for cpt2, region in enumerate(self.sequenceParser.getRegions()):
			transcript = Transcript()
			transcript.setName(region)
			transcript.setDirection("+")
			transcript.setEnd(self.sizes[region])
			transcript.setStart(1)
			output += self.plotTranscript(region, transcript)
			progress.inc()
		progress.done()
		if self.verbosity > 0:
			print output

	def plot2TranscriptFiles(self):
		self.outputCoverage          = [0] * self.parsers[1].getNbTranscripts()
		self.outputCoveragePerStrand = [None] * self.parsers[1].getNbTranscripts()
		for cpt in range(self.parsers[1].getNbTranscripts()):
			self.outputCoveragePerStrand[cpt] = dict([strand, 0] for strand in strands)
		progress = Progress(self.parsers[1].getNbTranscripts(), "Plotting regions", self.verbosity)
		output = ""
		for cpt2, transcript2 in enumerate(self.parsers[1].getIterator()):
			output += self.plotTranscript(cpt2, transcript2)
			progress.inc()
		progress.done()
		if self.verbosity > 0:
			print output

	def plot(self):
		if self.sequenceParser == None:
			self.plot2TranscriptFiles()
		else:
			self.plot1TranscriptFile()

	def start(self):
		self.initialize()
		self.compute()
		self.plot()


if __name__ == "__main__":
	
	# parse command line
	description = "Plot Coverage v1.0.1: Plot the coverage of the first data with respect to the second one. [Category: Visualization]"

	parser = OptionParser(description = description)
	parser.add_option("-i", "--input1",       dest="inputFileName1", action="store",                       type="string", help="input file 1 [compulsory] [format: file in transcript or mapping format given by -f]")
	parser.add_option("-f", "--inputFormat1", dest="inputFormat1",   action="store",                       type="string", help="format of input file 1 [compulsory] [format: transcript or mapping file format]")
	parser.add_option("-j", "--input2",       dest="inputFileName2", action="store",                       type="string", help="input file 2 [compulsory] [format: file in transcript format given by -g]")
	parser.add_option("-g", "--inputFormat2", dest="inputFormat2",   action="store",                       type="string", help="format of input file 2 [compulsory] [format: transcript file format]")
	parser.add_option("-q", "--sequence",     dest="inputSequence",  action="store",      default=None,    type="string", help="input sequence file [format: file in FASTA format] [default: None]")
	parser.add_option("-o", "--output",       dest="outputFileName", action="store",                       type="string", help="output file [compulsory] [format: output file in PNG format]")
	parser.add_option("-w", "--width",        dest="width",          action="store",      default=1500,    type="int",    help="width of the plots (in px) [format: int] [default: 1500]")
	parser.add_option("-e", "--height",       dest="height",         action="store",      default=1000,    type="int",    help="height of the plots (in px) [format: int] [default: 1000]")
	parser.add_option("-t", "--title",        dest="title",          action="store",      default="",      type="string", help="title of the plots [format: string]")
	parser.add_option("-x", "--xlab",         dest="xLabel",         action="store",      default="",      type="string", help="label on the x-axis [format: string]")
	parser.add_option("-y", "--ylab",         dest="yLabel",         action="store",      default="",      type="string", help="label on the y-axis [format: string]")
	parser.add_option("-p", "--plusColor",    dest="plusColor",      action="store",      default="red",   type="string", help="color for the elements on the plus strand [format: string] [default: red]")
	parser.add_option("-m", "--minusColor",   dest="minusColor",     action="store",      default="blue",  type="string", help="color for the elements on the minus strand [format: string] [default: blue]")
	parser.add_option("-s", "--sumColor",     dest="sumColor",       action="store",      default="black", type="string", help="color for 2 strands coverage line [format: string] [default: black]")
	parser.add_option("-l", "--lineColor",    dest="lineColor",      action="store",      default="black", type="string", help="color for the lines [format: string] [default: black]")
	parser.add_option("-1", "--merge",        dest="merge",          action="store_true", default=False,                  help="merge the 2 plots in 1 [format: boolean] [default: false]")
	parser.add_option("-D", "--directory",    dest="working_Dir",    action="store",      default=os.getcwd(), type="string", help="the directory to store the results [format: directory]")
	parser.add_option("-v", "--verbosity",    dest="verbosity",      action="store",      default=1,       type="int",    help="trace level [format: int]")
	(options, args) = parser.parse_args()

	colors[1]  = options.plusColor
	colors[-1] = options.minusColor
	colors[0]  = options.sumColor
	colorLine  = options.lineColor

	pp = PlotParser(options.verbosity)
	pp.addInput(0, options.inputFileName1, options.inputFormat1)
	pp.addInput(1, options.inputFileName2, options.inputFormat2)
	pp.addSequence(options.inputSequence)
	pp.setOutput(options.outputFileName if os.path.isabs(options.outputFileName) else os.path.join(options.working_Dir, options.outputFileName))
	pp.setPlotSize(options.width, options.height)
	pp.setLabels(options.xLabel, options.yLabel)
	pp.setTitle(options.title)
	pp.setMerge(options.merge)
	pp.start()