##########################################################################
# DbEntities.py:  Python source file for the Simcal1 module.             #
# Copyright (c) 2004 Daniel Dvorkin.                                     #
# Distributed under the Artistic License, WITHOUT "optional clause 8"    #
# (see http://www.opensource.org/licenses/artistic-license.php).         #
# All rights not granted in this license are reserved by the author.     #
##########################################################################

"""Defines DbEntity and its children.

DbEntity is the parent, not intended for direct access, for any class
definition covering genes and clusters.  Its child classes, Gene and Cluster, extend it with data and methods specific to genes and clusters 
respectively.  Only the child classes should be called from outside code.

Other classes, defined in other files, may inherit from the classes
defined here."""

# MASTER IMPORT STATEMENT

from Simcal1 import *
			
# PARENT CLASS DEFINITIONS (NOT TO BE ACCESSED FROM OUTSIDE)

class DbEntity(object):
	"""Parent class for information about database entities."""
	
	# CLASS VARIABLES
	
	# table variables
	Id = 0
	Exp1 = 0.00
	Exp2 = 0.00
	Exp3 = 0.00
	Variability = 0.00
	UserAnnotation = ""
	
	# query variables (redefine PRN in child classes)
	table = "DbEntities" # must redefine
	set = """Id = %s, Exp1 = %s, Exp2 = %s, Exp3 = %s, 
			 Variability = %s, UserAnnotation = %s""" # should redefine
	where = "Id = %s" # may need to redefine
	
	# CLASS QUERY METHODS
	
	def insert_query(cls):
		"""Returns a query string for use in inserts."""
		return "insert into %s set %s"%(cls.table, cls.set)
	insert_query = classmethod(insert_query)
	
	def update_query(cls):
		"""Returns a query string for use in updates."""
		return "update %s set %s where %s"%(cls.table, cls.set, cls.where)
	update_query = classmethod(update_query)
	
	# FREQUENTLY RETRIEVED VALUES METHODS
	
	def location(self):
		"""Returns a Numeric array representing location.  Note that the
		first element of the array is 0.00, representing "Exp0", the
		expression level of the untreated control -- this is important for
		calculating correlation."""
		
		return array([0.00, self.Exp1, self.Exp2, self.Exp3])
			
	def insert_tuple(self): # should redefine in child classes
		"""Return a tuple for use in insertion queries."""
		
		return(self.Id, self.Exp1, self.Exp2, self.Exp3,
			   self.Variability, self.UserAnnotation)
			
	def update_tuple(self): # may need to redefine in child classes
		"""Return a tuple for use in update queries."""
		
		return self.insert_tuple() + tuple([self.Id])
		
		
# CHILD CLASS DEFINITIONS (MAY BE ACCESSED FROM OUTSIDE)
				
class Gene(DbEntity):
	"""Class for numeric information about genes in the data space.
	
	Extends the DbEntity class with class variables and methods
	relating to calculations done specifically on genes."""
	
	# CLASS VARIABLES
	
	# table variables
	Reliability = 0.00
	AffyName = ""
	AffyAnnotation = ""
	
	# query variables
	table = "Genes"
	set = DbEntity.set + """, Reliability = %s, AffyName = %s,
							AffyAnnotation = %s"""
	
	# CLASS METHODS
	
	def do_cleanup(cls):
		"""Prepares for relationship-finding operations by deleting all
		existing entries in gene relationship tables."""
		
		setq("delete from GeneNeighbors")
		setq("delete from GeneSuccessionsIndividual")
		setq("delete from GeneSuccessions")
		
	do_cleanup = classmethod(do_cleanup)
	
	def get_group(cls, ClusterId = -1, Rank = 0):
		"""Returns a list of Gene objects ordered by Id.  If ClusterId and
		Rank are not set, the genes are all genes in the database.  If
		these arguments are set, then the genes are all genes belonging to
		the cluster so identified."""
		
		if ClusterId >= 0 and Rank > 0:
			query = """select Genes.*
					   from   ClusterGenes, Genes
					   where  ClusterId = %s and
					   		  Rank = %s and
					   		  Genes.Id = ClusterGenes.GeneId
					   order  by Genes.Id""" % (ClusterId, Rank)
		else:
			query = "select * from Genes order by Id"
			
		return [Gene(gene) for gene in mobq(query)]
	get_group = classmethod(get_group)
	
	def get_neighbors(cls):
		"""Returns a Numeric array of shape (MAX_RANK, NumOfGenes),
		where entry [i, j] is the (i+1)-rank neighbor of gene j."""
		
		neighbors = []
		for Rank in prange(MAX_RANK):
			theNeighbors = mobq("""select NeighborGeneId
								   from   GeneNeighbors
								   where  Rank = %s
								   order  by GeneId""", Rank)
			neighbors.append([theNeighbor.NeighborGeneId for
							  theNeighbor in theNeighbors])
							  
		return array(neighbors)
	get_neighbors = classmethod(get_neighbors)
	
	def set_neighbors(cls, NeighborTuples):
		"""Takes a sequence of tuples of the form (GeneId, NeighborId, 
		Rank), and inserts it into the database."""
		
		setq("""insert into GeneNeighbors
				set    GeneId = %s, 
					   NeighborGeneId = %s,
					   Rank = %s""",
			 *NeighborTuples)
			 
	set_neighbors = classmethod(set_neighbors)
	
	def set_individual_successions(cls, SuccessionTuples):
		"""Takes a sequence of tuples of the form (LeaderGeneId,
		FollowerGeneId, Rank), and inserts it into the database."""
		
		setq("""insert into GeneSuccessionsIndividual
				set    LeaderGeneId = %s, 
					   FollowerGeneId = %s,
					   Rank = %s""",
			 *SuccessionTuples)
	
	set_individual_successions = classmethod(set_individual_successions)
	
	def set_successions(cls):
		"""Performs calculations to yield the final set of gene succession
		information, with aggregate statistics."""
		
		setq("""insert into GeneSuccessions
				select distinct 
					   LeaderGeneId, 
					   FollowerGeneId,
					   Rank
				from   GeneSuccessionsIndividual
				order  by LeaderGeneId, FollowerGeneId, Rank""")
					   
	set_successions = classmethod(set_successions)
	
	# CONSTRUCTOR
	
	def __init__(self, theRecord = None):
		"""Takes an object of class DbRecord as an argument and fills
		in the appropriate information.  If called without arguments,
		instantiates an object with default values."""
        
		if theRecord != None:			   
			self.__dict__.update(theRecord.__dict__)
	
	# CALCULATION METHODS
	
	def set_cluster_succession(self):
		"""Inserts all cluster succession info with this gene as leader
		into the database."""
		
		setq("""insert into ClusterSuccessions
				select Leaders.ClusterId as LeaderClusterId,
					   Followers.ClusterId as FollowerClusterId,
					   Leaders.Rank as Rank,
					   count(*) as Prediction
				from   ClusterGenes as Leaders,
					   GeneSuccessions,
					   ClusterGenes as Followers
				where  Leaders.GeneId = %s and
					   Leaders.Rank = %s and
					   GeneSuccessions.Rank <= %s and
					   GeneSuccessions.LeaderGeneId = Leaders.GeneId and
					   Followers.Rank = %s and
					   Followers.GeneId = GeneSuccessions.FollowerGeneId
				group  by FollowerClusterId
				order  by FollowerClusterId""",
			 (self.Id, self.Rank, self.Rank, self.Rank))
		
	# FREQUENTLY RETRIEVED VALUES METHODS
	
	def insert_tuple(self):
		"""Return a tuple for use in insertion queries."""
		
		return DbEntity.insert_tuple(self) + \
			(self.Reliability,  self.AffyName, self.AffyAnnotation)
			   

class Cluster(DbEntity):
	"""Class for numeric information about clusters in the data space.
	
	Extends the DbEntity class with class variables and methods
	relating to calculations done specifically on clusters."""
	
	# CLASS VARIABLES
	
	# table variables
	Rank = 0
	Members = 0
	Reliability = 0.00
	ParentId = 0
	
	# query variables
	table = "Clusters"
	set = DbEntity.set + """, Rank = %s, Members = %s,
							Reliability = %s, ParentId = %s"""
	where = DbEntity.where + " and Rank = %s"
	
	# CLASS METHODS
	
	def do_cleanup(cls):
		"""Prepares for clustering operations by deleting all existing
		entries in Clusters and ClusterGenes."""
		
		setq("delete from Clusters")
		setq("delete from ClusterGenes")
		
	do_cleanup = classmethod(do_cleanup)
	
	def do_succession_cleanup(cls):
		"""Prepares for cluster succession-finding operations by deleting
		all existing entries in cluster succession tables."""
		
		setq("delete from ClusterSuccessions")
		setq("delete from ClusterSuccessionsIndividual")
	
	do_succession_cleanup = classmethod(do_succession_cleanup)
	
	def get_group(cls, Rank = 0):
		"""Returns a list of Cluster objects ordered by Rank (if set) and
		Id (always), in that order.  If Rank is not set, the clusters are 
		all clusters in the database.  If Rank is set, only clusters of
		that rank are returned."""
		
		if Rank > 0:
			query = """select * 
					   from   Clusters
					   where  Rank = %s
					   order  by Id""" % Rank
		else:
			query = """select * 
					   from   Clusters
					   order  by Rank, Id"""
			
		return [Cluster(cluster) for cluster in mobq(query)]
	get_group = classmethod(get_group)
	
	def do_create(cls, ClusterGenes, Rank):
		"""Takes a list of lists containing ID's of genes in clusters, of 
		the form "[[g1, g2, ...], [], ...]" and creates clusters in the 
		database.  Cluster calculations are performed.  Rank must be
		specified.  Returns the number of clusters created."""
		
		# prep work
		ClusterId = -1
		ClusterGeneTuples = []
		ClusterTuples = []
		
		# get data ready for insertion
		for GeneIds in ClusterGenes:
			if len(GeneIds):
				ClusterId += 1
				ClusterGeneTuples += \
					[(ClusterId, GeneId, Rank)
					 for GeneId in GeneIds]
				cluster = Cluster()
				cluster.Id = ClusterId
				cluster.Rank = Rank
				ClusterTuples.append(cluster.insert_tuple())
		
		# insert cluster genes and clusters
		setq("""insert into ClusterGenes
				set    ClusterId = %s,
					   GeneId = %s,
					   Rank = %s""", *ClusterGeneTuples)
		setq(cls.insert_query(), *ClusterTuples)
			 
		# do cluster calculations, update previous generation
		Clusters = cls.get_group(Rank)
		
		for i in xrange(len(Clusters)):
			Clusters[i].do_calculations()
			Children = mobq("""select distinct Child.ClusterId
										as ClusterId
							   from   ClusterGenes as Parent,
									  ClusterGenes as Child
							   where  Parent.ClusterId = %s and
									  Child.Rank = %s and
									  Child.GeneId = Parent.GeneId""",
							Clusters[i].Id, Rank-1)
			ChildTuples = [(Clusters[i].Id, Child.ClusterId, Rank-1)
						   for Child in Children]
			if(len(ChildTuples)):
				setq("""update Clusters
						set    ParentId = %s
						where  Id = %s and
							   Rank = %s""", *ChildTuples)
							   		    		  
		ClusterTuples = [cluster.update_tuple() for cluster in Clusters]
		setq(cls.update_query(), *ClusterTuples)
		
		return len(Clusters)
	do_create = classmethod(do_create)
	
	def get_genes(cls):
		"""Returns a list of cluster gene DbRecord objects."""
		
		return mobq("""select *
					   from   ClusterGenes
					   order  by Rank, ClusterId, GeneId""")

	get_genes = classmethod(get_genes)
	
	def set_individual_successions(cls, ClusterGene):
		"""Takes a cluster gene DbRecord object with the attributes 
		ClusterGene.ClusterId, ClusterGene.GeneId, and ClusterGene.Rank,
		and ..."""
		
		SetTuple = (ClusterGene.ClusterId, ClusterGene.Rank,
					ClusterGene.GeneId, ClusterGene.Rank,
					ClusterGene.Rank)
		
		setq("""insert into ClusterSuccessionsIndividual
				select %s as LeaderClusterId,
					   ClusterGenes.ClusterId as FollowerClusterId,
					   %s as Rank
				from   GeneSuccessions, ClusterGenes
				where  GeneSuccessions.LeaderGeneId = %s and
					   GeneSuccessions.Rank <= %s and
					   ClusterGenes.Rank = %s and
					   ClusterGenes.GeneId = 
							GeneSuccessions.FollowerGeneId""", SetTuple)
	
	set_individual_successions = classmethod(set_individual_successions)
	
	def set_successions(cls):
		"""Performs calculations to yield the final set of cluster 
		succession information, with aggregate statistics."""
		
		setq("""insert into ClusterSuccessions
				select LeaderClusterId, 
					   FollowerClusterId,
					   Rank,
					   count(*) as Prediction
				from   ClusterSuccessionsIndividual
				group  by LeaderClusterId, FollowerClusterId, Rank
				order  by LeaderClusterId, FollowerClusterId, Rank""")
					   
	set_successions = classmethod(set_successions)
					   
	# CONSTRUCTOR
	
	def __init__(self, theRecord = None):
		"""Takes an object of class DbRecord as an argument and fills
		in the appropriate information.  If called without arguments,
		instantiates an object with default values."""
        
		if theRecord != None:			   
			self.__dict__.update(theRecord.__dict__)
	
	# CALCULATION METHODS
	
	def do_calculations(self):
		"""Calculates location and reliability based on genes."""
					 
		info = objq("""select sum(Genes.Reliability * Genes.Exp1)
								/ sum(Genes.Reliability) as Exp1,
							  sum(Genes.Reliability * Genes.Exp2)
								/ sum(Genes.Reliability) as Exp2,
							  sum(Genes.Reliability * Genes.Exp3)
								/ sum(Genes.Reliability) as Exp3,
							  count(Genes.Id) as Members,
							  avg(Genes.Reliability) as Reliability
					   from   Genes, ClusterGenes
					   where  ClusterGenes.ClusterId = %s and
					   		  ClusterGenes.Rank = %s and
					   		  Genes.Id = ClusterGenes.GeneId""", 
					self.Id, self.Rank)
						
		self.__dict__.update(info.__dict__)
		
		self.Variability = (abs(self.Exp1) + abs(self.Exp2) + 
							abs(self.Exp3)) / 3.0
	
	def get_succession_info(self):
		"""Returns a list of tuples of the form (self.Id, 
		FollowerClusterId, self.Rank, Prediction), where FollowerId is the
		Id of a cluster which follows this cluster, and Prediction is the 
		number of genes which establishes this succession."""
		
		Followers = mobq("""select Followers.ClusterId 
									as FollowerClusterId,
								   count(*) as Prediction
							from   ClusterGenes as Leaders,
								   GeneSuccessions,
								   ClusterGenes as Followers
							where  Leaders.ClusterId = %s and
								   Leaders.Rank = %s and
								   GeneSuccessions.Rank <= %s and
								   GeneSuccessions.LeaderGeneId =
								   	Leaders.GeneId and
								   Followers.Rank = %s and
								   Followers.GeneId =
								   	GeneSuccessions.FollowerGeneId
							group  by FollowerClusterId
							order  by FollowerClusterId""",
						 self.Id, self.Rank, self.Rank, self.Rank)
						 
		return [(self.Id, Follower.FollowerClusterId, self.Rank,
				 Follower.Prediction) for Follower in Followers]
	
	def set_succession_info(self):
		"""Inserts all succession info with this cluster as leader
		into the database."""
		
		setq("""insert into ClusterSuccessionsIndividual
				select %s as LeaderClusterId,
					   Followers.ClusterId as FollowerClusterId,
					   %s as Rank
				from   ClusterGenes as Leaders,
					   GeneSuccessions,
					   ClusterGenes as Followers
				where  Leaders.ClusterId = %s and
					   Leaders.Rank = %s and
					   GeneSuccessions.Rank <= %s and
					   GeneSuccessions.LeaderGeneId = Leaders.GeneId and
					   Followers.Rank = %s and
					   Followers.GeneId = GeneSuccessions.FollowerGeneId
				order  by FollowerClusterId""",
			 (self.Id, self.Rank, self.Id, 
			  self.Rank, self.Rank, self.Rank))
					   
		
	# FREQUENTLY RETRIEVED VALUES METHODS
		
	def insert_tuple(self):
		"""Return a tuple for use in insertion queries."""
	
		return DbEntity.insert_tuple(self) + \
			(self.Rank, self.Members, self.Reliability, self.ParentId)
			
	def update_tuple(self): # may need to redefine in child classes
		"""Return a tuple for use in update queries."""
		
		return DbEntity.update_tuple(self) + tuple([self.Rank])