File: fitGCP.py

package info (click to toggle)
fitgcp 0.0.20150429-4
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 23,312 kB
  • sloc: python: 1,551; sh: 49; makefile: 7
file content (795 lines) | stat: -rw-r--r-- 24,506 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
#!/usr/bin/python3
"""
Created on Fri Aug 31 2012 14:05

Copyright (c) 2012, Martin S. Lindner and Maximilian Kollock,
Robert Koch-Institut, Germany,
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * The name of the author may not be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL MARTIN S. LINDNER OR MAXIMILIAN KOLLOCK BE LIABLE 
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

usage = """
%prog [options] NAME

Help on python3 script fitGCP.py

Fits mixtures of probability distributions to genome coverage profiles using an
EM-like iterative algorithm.

The script uses a SAM file as input and parses the mapping information and 
creates a Genome Coverage Profile (GCP). The GCP is written to a file, such that
this step can be skipped the next time.
The user provides a mixture model that is fitted to the GCP. Furthermore, the 
user may specify initial parameters for each model. 

As output, the script generates a text file containing the final set of fit 
parameters and additional information about the fitting process. A log file
contains the the current set of parameters in each step of the iteration. If 
requested, a plot of the GCP and the fitted distributions can be created.

PARAMETER:

NAME: Name of SAM file to analyze.
"""


import pysam
import numpy as np
import scipy.stats as stats
import sys
import os
from collections import namedtuple
import time
import pickle
from optparse import OptionParser
from scipy.special import digamma, betainc
from scipy.optimize import newton


"""----------------------------------------------------------------------------
    Define the distributions
----------------------------------------------------------------------------"""

class Distribution:
	_p1 = None
	_p2 = None
	_name = "General Distribution"
	_dof = 0 # Number of degrees of freedom

	alpha = 1.
	def __str__(self):
		""" return the name of the distribution """
		return self._name
	
	def set_par(self,p1=None, p2=None):
		""" explicitly set a parameter """
		if p1 != None:
			self._p1 = p1
		if p2 != None:
			self._p2 = p2

	def pmf(self, x):
		""" return the value of the probability mass function at x """
		return x*0.

	def estimate_par(self, data=None, weights=None):
		""" estimate the distribution parameters from the data and the weights 
		(if provided) """
		pass

	def init_par(self, mean=None, var=None):
		""" estimate initial distribution parameters given the mean and 
		variance of the data"""
		pass

	def report_stats(self, width=20):
		""" return a string that reports information about the distribution """
		return str(self._name).ljust(width) + str(self.alpha).ljust(width) + \
	           str(self._p1).ljust(width) + str(self._p2).ljust(width)


class Zero(Distribution):
	_name = "Zero"
	_dof = 1

	def pmf(self,x):
		if isinstance(x,np.ndarray):
			return (x==0).astype(np.float)
		else:
			return float(x==0)


class NBinom(Distribution):
	_name = "NBinom"
	_dof = 3
	_use_MOM = False
	
	def pmf(self, x):
		return stats.nbinom.pmf(x,self._p1,self._p2)

	def estimate_par(self, data, weights=None):
		if weights is None:
			weights = data*0. + 1.
		norm = np.sum(weights)
		mean = np.sum(data*weights)/(norm + 10**(-25))
		var = np.sum((data - mean)**2 * weights) / (norm + 10**(-25))

		if self._use_MOM:
			if var < mean:
				print("Warning: var < mean")
				var = 1.01*mean
			self._p1 = mean**2 / (var - mean)
			self._p2 = mean / var
		else:
			def dp1_llnbinom(param,obs,obs_w):
				# param: parameter 1
				# obs:   observed values
				# obs_w: weight of each value
				N = np.sum(obs_w)
				data_mean = np.sum(obs*obs_w)/(N)
				return np.sum(digamma(obs+param)*obs_w) - N*digamma(param) + \
			           N*np.log(data_mean/(param+data_mean))
			try:
				self._p1 = newton(dp1_llnbinom,self._p1, args=(data,weights),
								  maxiter=10000)
				self._p2 = (self._p1)/(self._p1+mean)
			except:
				print("Warning: MLE for negative binomial failed. Using MOM.")
				if var < mean:
					print("Warning: var < mean")
					var = 1.01*mean
				self._p1 = mean**2 / (var - mean)
				self._p2 = mean / var
				


	def report_stats(self, width=20):
		""" return a string that reports information about the distribution """
		return str(self._name).ljust(width) + str(self.alpha).ljust(width) + \
	           str(self._p1).ljust(width) + str(self._p2).ljust(width) + \
			   str(stats.nbinom.mean(self._p1,self._p2)).ljust(width)


		
class Poisson(Distribution):
	_name = "Poisson"
	_dof = 2

	def pmf(self,x):
		return stats.poisson.pmf(x,self._p1)

	def estimate_par(self, data, weights=None):
		mean = np.sum(data*weights) / (np.sum(weights) )
		self._p1 = mean



class TailDistribution(Distribution):
	_name = "Tail"
	_dof = 1
	_norm = False  # normalization; recalculate only if necessary
	_parent = None
	_active = True # switch tail on/off

	def set_par(self,p1=None, p2=None):
		""" explicitly set a parameter """
		if p1 != None:
			self._p1 = p1
		if p2 != None:
			self._p2 = p2
		self._norm = False

	def estimate_par(self, data=None, weights=None):
		""" Do not estimate parameters, but obtain parameters from parent 
		distribution """
		self._p1 = self._parent._p1
		self._p2 = self._parent._p2
		self._norm = False



class NbTail(TailDistribution):
	_name = "Tail"
	_dof = 1
	def __init__(self, nbinom):
		""" NbTail distribution must be connected to a negative binomial"""
		if isinstance(nbinom, NBinom):
			self._parent = nbinom
		else:
			raise Exception
	
	def pmf(self, x):
		if np.isscalar(x) and x == 0:
			return 0.
		
		if self._active == False:
			return 0*x
		if stats.nbinom.mean(self._p1, self._p2) < 1.:
			self._active = False # switch tail permanently off

		def betaincreg(x,p1,p2):
			return 1-betainc(p1,x+1,p2)

		# calculate normalization up to certain precision
		if not self._norm:
			ks = int(max(2,stats.nbinom.ppf(0.999999,self._p1,self._p2)))
			norm = np.sum(betaincreg(np.arange(1,ks),self._p1,self._p2))
		else:
			norm = self._norm
		# now return the value(s)
		ret = betaincreg(x,self._p1,self._p2) / norm
		if not np.isscalar(x):
			ret[np.where(x==0)] = 0.
		return ret



class PoissonTail(TailDistribution):
	_name = "Tail of Poisson"
	_dof = 1
	def __init__(self, poisson):
		""" PoissonTail distribution must be connected to a poisson 
		distribution """
		if isinstance(poisson, Poisson):
			self._parent = poisson
		else:
			raise Exception
	
	def pmf(self, x):
		if self._p1 < 1.:
			self._active = False # switch tail permanently off
		if self._active == False:
			return 0*x

		if np.isscalar(x) and x == 0:
			return 0.

		xmax = int(max(np.max(x),5,stats.poisson.ppf(0.999999,self._p1)))+1
		xs = np.arange(0,xmax, dtype=np.float)
		#backward cumulative sum
		tx = np.cumsum((stats.poisson.pmf(xs, self._p1)/xs)[::-1])[::-1]
		tx[0] = 0
		tx /= np.sum(tx)
		return tx[x]


def build_mixture_model(dist_str):
	""" Build a mixture model from a string """
	num_dist = len(dist_str)
	mm = np.array([Zero()]*num_dist)
	it = 0	
	for dist in dist_str:
		if dist == "z":
			it += 1
		elif dist=="n":
			mm[it] = NBinom()
			mm[it]._use_MOM = True
			it += 1
		elif dist=="N":
			mm[it] = NBinom()
			mm[it]._use_MOM = False
			it += 1
		elif dist == "t":
			if it > 0 and isinstance(mm[it-1], NBinom):
				mm[it] = NbTail(mm[it-1])
				it += 1
			elif it > 0 and isinstance(mm[it-1], Poisson):
				mm[it] = PoissonTail(mm[it-1])
				it += 1
			else:
				raise Exception("Error: wrong input '%s'. A Tail distribution "
								"(t) must follow a Negative Binomial (n|N) or "
								"Poisson (p)."%dist_str)
		elif dist == "p":
			mm[it] = Poisson()
			it+=1
		else:
			raise Exception("Input Error: distribution %s not recognized!"%dist)

	# initialize all distributions with equal weights
	alpha = 1./len(mm)
	for dist in mm:
		dist.alpha = alpha
		
	return mm


def init_gamma(mixture_model, dataset):
	""" create initial responsibilities gamma. The probability that a coverage 
	belongs to a distribution is equal for all distributions."""
	N_mm = len(mixture_model)
	N_cov = len(dataset.cov)
	
	return np.array([[1./N_mm for d in mixture_model] for i in range(N_cov)])



"""----------------------------------------------------------------------------
    Define DataSet to store all relevant information (incl. GCP)
----------------------------------------------------------------------------"""

class DataSet:
	fname = ""                        # path to original SAM file
	cov = np.array([],dtype=np.int)   # observed coverage values
	count = np.array([],dtype=np.int) # number of observations for each cov
	rlen = 0                          # average read length of mapped reads
	rds = 0                           # total number of reads
	glen = 0                          # length of the genome
	
	def read_from_pickle(self,filename):
		""" read a genome coverage profile from a pickle file. """
		data = pickle.load(open(filename,'rb'))

		self.cov = np.array(data[0],dtype=np.int)
		self.count = np.array(data[1],dtype=np.int)
		self.rlen = data[2]
		self.rds = data[3]
		self.glen = data[4]
		if len(data) == 6:
			self.fname = data[5] 


	def read_from_sam(self, filename):
		""" extract a genome coverage profile from a sam file. """
		sf = pysam.Samfile(filename,'r')
	
		cov = np.zeros((sum(sf.lengths),))
		start_pos = np.zeros((len(sf.lengths),))
		start_pos[1:] = np.cumsum(sf.lengths[:-1])

		read_length = 0
		num_reads = 0
		for read in sf:
			if not read.is_unmapped:
				r_start = start_pos[read.tid] + read.pos # start position 
				r_end = start_pos[read.tid] + read.pos + read.qlen # end 
				cov[int(r_start):int(r_end)] += 1
				num_reads += 1
				read_length += r_end-r_start
		self.fname = filename
		self.rds = num_reads
		self.rlen = read_length/float(num_reads)  # average length mapped reads
		self.glen = len(cov)
		self.cov = np.unique(cov)
		self.count = np.array( [np.sum(cov==ucov) for ucov in self.cov] )
	
	def write_to_pickle(self, filename):
		""" store dataset in a pickle file. """
		return pickle.dump([self.cov, self.count, self.rlen, self.rds,
							 self.glen, self.fname], open(filename,'wb'))
	


"""----------------------------------------------------------------------------
    Iterative Method function definitions
----------------------------------------------------------------------------"""


def update_gamma(data_set, mixture_model, gamma):
	""" Update the probability gamma_i(x), that a position with coverage x 
	belongs to distribution i
	"""
	num_dist = len(mixture_model)
	for it in range(len(data_set.cov)):
		# calculate probability that coverages[it] belongs to a distribution
		prob = [dist.alpha*dist.pmf(data_set.cov[it]) for dist in mixture_model]
		if sum(prob) <= 0:
			gamma[it,:] = 0.
		else:
			gamma[it,:] = np.array([prob[i]/sum(prob) for i in range(num_dist)])
	
	return 1


def update_alpha(data_set, mixture_model, gamma):		
	""" Update alpha_i, the proportion of data that belongs to 
	distribution i """

	for i in range(len(mixture_model)-1):
		w_probs = np.array([p*w for p,w in zip(gamma[:,i],data_set.count)])
		mixture_model[i].alpha = np.sum(w_probs) / np.sum(data_set.count)

	mixture_model[-1].alpha = 1 - sum([d.alpha for d in mixture_model[:-1]])

	return 1


def iterative_fitting(data_set, mixture_model, gamma, iterations):
	""" Generator fuction to run the iterative method. Operates directly on the
	data structures mixture_model and gamma. """
	
	for i in range(iterations):
		# Expectation-step: update gammas
		update_gamma(data_set, mixture_model, gamma)
		
		# temporarily store old parameters
		old_p1 = np.array([d._p1 or 0 for d in mixture_model])
		old_p2 = np.array([d._p2 or 0 for d in mixture_model])
		old_alpha = np.array([d.alpha for d in mixture_model])

		# Parameter estimation step
		for j,dist in enumerate(mixture_model):
			dist_weights = gamma[:,j]*data_set.count
			dist.estimate_par(data_set.cov, dist_weights)
		update_alpha(data_set, mixture_model, gamma)

		# calculate relative change of the parameters
		new_p1 = np.array([d._p1 or 0 for d in mixture_model])
		new_p2 = np.array([d._p2 or 0 for d in mixture_model])
		new_alpha = np.array([d.alpha for d in mixture_model])
		
		rel_p1 = np.max(np.abs(new_p1-old_p1) / (new_p1 + 1e-20))
		rel_p2 = np.max(np.abs(new_p2-old_p2) / (new_p2 + 1e-20))
		rel_alpha = np.max(np.abs(new_alpha-old_alpha) / (new_alpha + 1e-20))

		max_change = np.max([rel_p1, rel_p2, rel_alpha])

		# maximum CDF difference
		xs = np.arange(np.max(data_set.cov)+1)
		ref_pdf = np.zeros((int(np.max(data_set.cov))+1,))
		for dist in mixture_model:
			ref_pdf += dist.pmf(xs)*dist.alpha
		
		obs_pdf = np.zeros((int(np.max(data_set.cov))+1,))
		obs_pdf[data_set.cov.astype(np.int)] = data_set.count/float(np.sum(data_set.count))
		max_cdf_diff = np.max(np.abs(np.cumsum(ref_pdf)-np.cumsum(obs_pdf)))

		yield i, max_change, max_cdf_diff


"""----------------------------------------------------------------------------
    Auxiliary code
----------------------------------------------------------------------------"""

class Logger:
	""" simple logger class """
	log_file = None
	def __init__(self, filename=None):
		if filename:
			self.log_file = open(filename,'w')

	def log(self, s, c=True):
		""" write string s to log file and print to console (if c) """
		if c:
			print(s)
		if self.log_file:
			self.log_file.write(s+'\n')


def create_plot(ds, mm, filename):
	""" create a plot showing the data and the fitted distributions """
	plt.figure()
	xmax = np.max(ds.cov)+1
	plotXs = np.arange(xmax)
	plt.plot(ds.cov[np.where(ds.cov<xmax)],ds.count[np.where(ds.cov<xmax)]/
			 float(ds.glen),'k', lw=2, label="GCP")
	plotY_total = np.zeros((len(plotXs),))
	for dist in mm:
		if isinstance(dist,Poisson):
			format_string = "--"
			color = 'r'
		elif isinstance(dist,NBinom):
			format_string = "-."
			color = 'b'
		elif isinstance(dist,TailDistribution):
			format_string = ":"
			color = 'Purple'
		elif isinstance(dist,Zero):
			format_string = "-"
			color = 'YellowGreen'

		plotYs = dist.pmf(plotXs)*dist.alpha
		plotY_total += plotYs
		plt.plot(plotXs,plotYs, format_string, color=color, lw=2, label=dist._name)
	plt.plot(plotXs,plotY_total,'--',color='gray',lw=2, label="Mixture")
	plt.xlabel('coverage')
	plt.ylabel('')
	plt.xlim(xmin=0, xmax=xmax)
	plt.legend()
	plt.savefig(filename, dpi=300, bbox_inches='tight')



"""----------------------------------------------------------------------------
    Main function
----------------------------------------------------------------------------"""

if __name__ == "__main__":

	# Define command line interface using OptionParser
	parser = OptionParser(usage=usage)
	
	parser.add_option("-d", "--distributions", dest="dist",	
	help="Distributions to fit. z->zero; n: nbinom (MOM); N: nbinom (MLE); "
	"p:binom; t: tail. Default: %default", default="zn")
	
	parser.add_option("-i", "--iterations", dest="steps", type='int', 
	help="Maximum number of iterations. Default: %default", default=50)
	
	parser.add_option("-t", "--threshold", dest="thr", type='float', help="Set"
	" the convergence threshold for the iteration. Stop if the change between "
	"two iterations is less than THR. Default: %default", default=0.01)
	
	parser.add_option("-c", "--cutoff", dest="cutoff", type='float', 
	help="Specifies a coverage cutoff quantile such that only coverage values"
	" below this quantile are considered. Default: %default", default=0.95)
	
	parser.add_option("-p", "--plot", action="store_true", dest="plot", 
	help="Create a plot of the fitted mixture model. Default: %default",
	default=False)
	
	parser.add_option("-m", "--means", dest="mean", type='float', 
	action="append", help="Specifies the initial values for the mean of each "
	"Poisson or Negative Binomial distribution. Usage: -m 12.4 -m 16.1 will "
	"specify the means for the first two non-zero/tail distributions. The "
	"default is calculated from the data.", default=None)
	
	parser.add_option("-a", "--alpha", dest="alpha", action="append", 
	help="Specifies the initial values for the proportion alpha of each "
	"distribution. Usage: For three distributions -a 0.3 -a 0.3 specifies the "
	"proportions 0.3, 0.3 and 0.4. The default is "
	"equal proportions for all distributions.", default=None)
	
	parser.add_option("-l", "--log", action="store_true", dest="log", 
	help="Enable logging. Default: %default", default=False)

	parser.add_option("--view", action="store_true", dest="view", 
	help="Only view the GCP. Do not fit any distribution. Respects cutoff "
					  "(-c). Default: %default", default=False)

	(options, args) = parser.parse_args()
	
	if len(args) != 1:
		parser.print_help()
		sys.exit(1)


	# process command line input

	# check input file name
	name = args[0] # filename is the first (and only) argument
	if not os.path.exists(name):
		raise Exception("Could not find file with name '%s'."%name)
	if name.endswith('.sam'):
		name = name[:-4]


	# enable logging
	if options.log and not options.view:
		logfile = name + '_%s_log.txt'%options.dist
	else:
		logfile = None
	lg = Logger(logfile)

	
	# load data
	DS = DataSet()
	if os.path.exists(name+'.pcl'):
		print('found pickle file')
		DS.read_from_pickle(name+'.pcl')
	else:
		DS.read_from_sam(name+'.sam')
		DS.write_to_pickle(name+'.pcl')


	# weight cutoff. Coverages above this value have no weight in parameter estimation
	try:
		cutoff = max(int(np.max(DS.cov[np.where(np.cumsum(DS.count)<= \
				 options.cutoff*DS.glen)])+1),10)
	except:
		cutoff = np.max(DS.cov)+1
	lg.log("Using coverage cutoff: %i"%cutoff)

	DS.count = DS.count[np.where(DS.cov < cutoff)]
	DS.cov = DS.cov[np.where(DS.cov < cutoff)]
	num_unique = len(DS.cov)
	DS.glen = np.sum(DS.count)
	mean_cov = np.sum(DS.cov*DS.count)/np.sum(DS.count).astype(np.float)


	# only view the GCP
	if options.view:
		try:
			import matplotlib.pyplot as plt
		except:
			raise Exception("Error: could not import matplotlib.")
		
		# create empty mixture model
		MM = np.array([])
		create_plot(DS,MM,name+'.png')
		print("Wrote GCP plot to file %s."%(name+'.png'))
		sys.exit(0)


	# Plotting: check if matplotlib is installed
	plot = options.plot		
	if plot:
		try:
			import matplotlib.pyplot as plt
		except:
			lg.log("Warning: could not import matplotlib. Plotting disabled.")
			plot = False

	# create the mixture model
	if options.dist.count('z') > 1:
		raise Exception("Error: only one Zero disribution is allowed!")

	if options.dist.count("t") > 1:
		lg.log("Warning: more than one tail distribution may yield inaccurate "
		"estimates!")

	MM = build_mixture_model(options.dist)
	num_dist = len(MM)
	the_zero = options.dist.find('z')
	
	if options.alpha != None:
		if len(options.alpha) >= num_dist:
			raise Exception("Error: the number of alpha values (%i) exceeds "
			"the number of distributions (%i)!"%(len(options.alpha,num_dist)))


	# set initial proportions for each distribution
	if options.alpha:
		alpha = options.alpha
		if len(options.alpha) < len(MM):
			rest_alpha = 1. - sum(options.alpha)
			rest_models = len(MM) - len(options.alpha)
			alpha.append([rest_alpha / rest_models] * rest_models)			
		
		for dist,a in zip(MM,alpha):
			dist.alpha = float(a)
		
	# initialize distribution parameters
	n_dist = options.dist.count('n') + options.dist.count('N') + \
		     options.dist.count('p')
	factors = np.linspace(-n_dist,n_dist,n_dist)
	data_mean = np.power(np.abs(factors),np.sign(factors))*np.mean(DS.cov[1:]*\
				DS.count[1:]/np.mean(DS.count))

	if options.mean:
		# use mean values provided by the user, where possible
		for i,m in enumerate(options.mean):
			data_mean[i] = m

	data_var = np.array([np.sum((DS.cov-m)**2*DS.count)/DS.glen \
						 for m in data_mean])

	it = 0
	for dist in MM:
		if isinstance(dist,Zero) or isinstance(dist,NbTail) \
		or isinstance(dist,PoissonTail):
			dist.estimate_par()
			continue
		if isinstance(dist,NBinom):
			m,v = data_mean[it],data_var[it]
			if m > v:
				v = 1.01*m
			dist.set_par(m**2 / (v - m) , m / v )
			it += 1
			continue
		if isinstance(dist,Poisson):
			dist.set_par(data_mean[it])
			it += 1


	# set initial values for the responsibilities gamma
	GAMMA = init_gamma(MM,DS)
	

	# write initial values to log file
	lg.log('Initial values',False)
	lg.log('distribution'.ljust(20)+'alpha'.ljust(20)+'parameter 1'.ljust(20)+
		   'parameter 2',False)
	for i in range(num_dist):
		lg.log(str(MM[i]).ljust(20)+str(MM[i].alpha).ljust(20)+
			   str(MM[i]._p1).ljust(20)+str(MM[i]._p2),False)
	lg.log('\n',False)



	# run the iteration, repeatedly update the variables MM and GAMMA
	t_start = time.time()
	for i,change,diff in iterative_fitting(DS, MM, GAMMA, options.steps):	
		print(i)
		t_step = time.time() - t_start
		lg.log(('step#: '+str(i+1)).ljust(20)+'time: '+str(round(t_step,1))+'s'
			   +'\n',False)
		lg.log('distribution'.ljust(20)+'alpha'.ljust(20)+
			   'parameter 1'.ljust(20)+'parameter 2',False)
		for d in MM:
			lg.log(d.report_stats(20),False)
		lg.log('Max. CDF diff:'.ljust(20)+'%f'%diff,False)
		lg.log('\n',False)

		if change < options.thr:
			break
		t_start = time.time()
	
	

	# Estimate genome fragmentation and correct the zero-coverage estimate,
	# if a tail distribution was used
	zero_frac = MM[the_zero].alpha
	tail = False
	for dist in MM:
		if isinstance(dist,PoissonTail) and dist.alpha > 0:
			p_tail   = dist.alpha
			p_parent = dist._parent.alpha
			part_cov = dist._parent._p1
		elif isinstance(dist,NbTail) and dist.alpha > 0:
			p_tail   = dist.alpha
			p_parent = dist._parent.alpha
			part_cov = stats.nbinom.mean(dist._parent._p1,dist._parent._p2)
		else:
			continue
		
		tail=True
		gfrag = DS.glen/(1.+p_parent/p_tail)/2./DS.rlen*(p_parent+p_tail)
			
		start_prob = 1. - stats.poisson.cdf(0,part_cov/float(DS.rlen))
		zero_corr = (2*gfrag-1)*(min(stats.nbinom.mean(1,start_prob),DS.rlen) 
								 - start_prob/3.*4./9.)
		zero_est = DS.glen*MM[the_zero].alpha
		zero_frac = (zero_est-zero_corr)/float(DS.glen)


	# write results to file
	res = open(name+'_'+str(options.dist)+'_results.txt','w')
	res.write('Genome length:       %i\n'%DS.glen)
	res.write('Number of reads:     %i\n'%DS.rds)
	res.write('Average read length: %i\n'%DS.rlen)
	#res.write('Average Coverage:    %f\n'%mean_cov)
	res.write('Max. CDF difference: %f\n\n'%diff)
	res.write('Distribution'.ljust(20)+'alpha'.ljust(20)+
			  'parameter 1'.ljust(20)+'parameter 2'.ljust(20)+'[Mean]\n')
	for d in MM:
		res.write(d.report_stats(20)+'\n')
	
	if tail:
		res.write('\nNum. Fragments:'.ljust(20)+'%.01f'%gfrag+'\n')
		res.write('Observed Zeros:'.ljust(20)+
				  '%.04f'%(zero_est/float(DS.glen))+'\n')
		res.write('Corrected Zeros:'.ljust(20)+
				  '%.04f'%((zero_est-zero_corr)/float(DS.glen))+'\n')
	res.close()

	lg.log('\nFinal Results:\n')
	#lg.log('Average Coverage:    %f'%mean_cov)
	lg.log('Max. CDF difference: %f\n'%diff)
	lg.log('Distribution'.ljust(20)+'alpha'.ljust(20)+'parameter 1'.ljust(20)+
		   'parameter 2'.ljust(20)+'[Mean]')
	for d in MM:
		lg.log(d.report_stats(20))
		
	if tail:
		lg.log('Num. Fragments:'.ljust(20)+'%.01f'%gfrag)
		lg.log('Observed Zeros:'.ljust(20)+'%.04f'%(zero_est/float(DS.glen)))
		lg.log('Corrected Zeros:'.ljust(20)+'%.04f'%((zero_est-zero_corr)/
													 float(DS.glen)))


	# plot the figure, if requested
	if plot:
		create_plot(DS,MM,name+'_'+options.dist+'.png')

	print("\nFinished.")