#!/usr/bin/env python
"""
A python module with logging to animate a reversible
birth-death-immigration process.

Metadata:

  - Author:    W.S.Kendall
  - Copyright: opensource (c) W.S.Kendall 2008
  - Date:      2008-07-04
  - URL:       http://www.wilfridkendall.co.uk
  - Generator: '/home/wilfrid/lib/python/generate.py' version 1.18.


Command-line options::

  usage: %prog [options] arguments
    --logging=LOGGING:	   logging verbosity level.

Notes:
 - values for 'logging' option:
 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'
 (decreasing level of verbosity), with 'WARNING' as default.

"""
__RCSID__ = '$Revision: 1.2 $ $Date: 2008/07/14 13:53:11 $'


# \section{Definitions}
# We need the logging module regardless of whether the #logging# option
# is deployed, since #logging.info# calls are scattered through the text.
import logging

from Tkinter import BOTH, Checkbutton, DoubleVar, IntVar, \
    LAST, Scale, Tk, VERTICAL, X, Y

from bdi import BDI

from display2 import ITALICS, BOLD, Display2

#\section{Classes}
class AnimateBDI(Display2):
	"""
	Presents animation of reversible birth-death-immigration process.
	"""
	def __init__(self, root, x0=0, x1=20, y0=0, y1=15):
		Display2.__init__(self, root, xlo=x0, xhi=x1, ylo=y0, yhi=y1)	


		# Buttons for actions.
		self.button(text='SIMULATE', font=BOLD, foreground='darkgreen',\
				    command=self.simulate)
		self.button(text='ANALYZE', font=BOLD, foreground='darkgreen', \
				    command=self.analyze)
		self.button(text='ITERATE', font=BOLD, foreground='darkgreen', \
				    command=self.iterate)
		
		# Scales for adjustments.
		self.alpha = DoubleVar()
		self.alpha.set(1)
		Scale(self.buttonframe, from_=10.0, to=0.0, \
				      resolution=0.1, \
				      orient=VERTICAL, width=20, \
				      label='alpha', font=BOLD, \
				      variable=self.alpha, \
				      command=self.wipe).pack(fill=BOTH)

		self.lambd = DoubleVar()
		self.lambd.set(1)
		Scale(self.buttonframe, from_=10.0, to=0.0, \
				      resolution=0.1, \
				      orient=VERTICAL, width=20, \
				      label='lambda', font=BOLD, \
				      variable=self.lambd, \
				      command=self.wipe).pack(fill=BOTH)

		self.mu = DoubleVar()
		self.mu.set(2)
		Scale(self.buttonframe, from_=10.0, to=0.0, \
				      resolution=0.1, \
				      orient=VERTICAL, width=20, \
				      label='mu', font=BOLD, \
				      variable=self.mu, \
				      command=self.wipe).pack(fill=BOTH)

		self.batch = IntVar()
		self.batch.set(200)

		# Buttons for adjustments.
		self.toggle = IntVar()
		self.toggle.set(True)
		self.togglecb = Checkbutton(self.buttonframe, \
						    foreground='black', \
						    text='EQUILIBRIUM', \
						    width = 11,
						    font=BOLD, \
						    variable=self.toggle, \
						    command=self.togglewipe)
		self.togglecb.pack(fill=X)

		self.button(text='WIPE', font=BOLD, foreground='firebrick', \
				    command=self.wipe)

		for yinc in range(y0+1, y1+1):
			self.axis(x0, yinc, x1, fill='lightgray')
		
		self.x0, self.x1, self.y0, self.y1 = x0, x1, y0, y1
		
		self.chain, self.rev_trans, self.rev_delay = None, [], []

		for i in range(self.y0, self.y1+1):
			self.rev_trans.append([0, 0])
			self.rev_delay.append([0, 0.0])

		self.iterations = 1
		
	def simulate(self):
		"""
		Run a single animation.
		"""
		if self.chain is not None:
			return
		
		self.canvas.delete('active')
		self.chain = BDI(alpha=self.alpha.get(), \
					 lambd=self.lambd.get(), \
					 mu=self.mu.get())
		if self.toggle.get():
			self.chain.equilibrium_draw()
		else:
			self.chain[:] = [(0, self.chain.rng.choice([0, 10]))]
		loc = self.chain[-1]
		self.dot(loc, fill='red', width=4, tags='active')

		data = {}

		while self.chain[-1][0] < self.x1:
			self.chain.update()

			if self.chain[-1][0] > self.x1:
				self.chain[-1] = (self.x1, self.chain[-1][1])
			loc = self.chain[-1]
			self.dot(loc, fill='midnightblue', \
					 tags='active')
		self.canvas.update_idletasks()
		self.iterations += 1
	
	def analyze(self):
		"""
		Analyze last animation.
		"""
		if self.chain is None:
			return

		self.canvas.delete('transient')
		T = self.x1
		for transition in self.chain.reversed_statistics():
			t, start, finis = transition
			# Note we record under #finis# not #start#!
			if finis > self.y1:
				pass
			elif start == finis:
				raise Error, start
			else:
				self.rev_delay[finis][0] += 1
				self.rev_delay[finis][1] += t

			# Backwards version!
			self.dot((T, finis), outline='', fill='lightblue', \
					 tags='transient')
			self.line(T, start, T, finis, \
					  fill='red', arrow=LAST, \
					  width=2, tag='transient')
			self.line(T, finis, T - t, finis, \
					  fill='orange', \
					  width=2, tag='transient')

			T -= t
		
		def delay_plus(x):
			if x[0] == 0:
				return '???'
			else:
				return '%3.1f' % (float(x[0]) / x[1])
		self.canvas.delete('prob')
		for i, txt in enumerate(delay_plus(x) for x in self.rev_delay):
			self.write(txt, -0.6, i, font=BOLD, tags='prob') # use  fill='darkgreen' to change colour
				
		self.chain = None

	def iterate(self):
		m, dm = 10, self.batch.get()
		for i in range(self.iterations, self.iterations + dm):
			self.simulate()
			self.analyze()
			if not i % m:
				self.canvas.delete('record')
				self.write('[%4d]' % i, -0.5, -0.5, \
						   font=ITALICS, tags='record')
				self.canvas.update_idletasks()
		self.iterations += dm
	
	def togglewipe(self):
		"""
		Wipe all objects and change checkbutton appearance.
		"""
		if self.toggle.get():
			self.togglecb['text'] = 'EQUILIBRIUM'
			self.togglecb['foreground'] = 'black'
		else:
			self.togglecb['text'] = '2 PT START '
			self.togglecb['foreground'] = 'red'
		self.wipe()

	def wipe(self, *args):
		"""
		Wipe all objects.
		"""
		if self.lambd.get() >= self.mu.get():
			self.lambd.set(self.mu.get() * 0.5)

		self.chain, self.iterations = None, 1
		self.rev_trans, self.rev_delay = [], []
		for i in range(self.y0, self.y1 + 1):
			self.rev_trans.append([0, 0])
			self.rev_delay.append([0, 0.0])
		
		self.canvas.delete('objects', 'active', 'transient', 'prob', 'record')


#\section{Exceptions}
class Error(Exception):
	"""
	Generic exception to be raised by this module.
	"""

# \section{Functions}
def main(opt, arguments):
	"""
	The action of the module as script should be concentrated here!
	"""
	root = Tk()
	root.title('Reversible birth-death-immigration process')
	animation = AnimateBDI(root)
	
	root.mainloop()




# \section{Main script}
if __name__ == '__main__':
	import optionparse
	option, args = optionparse.parse(__doc__, version=__RCSID__)

	if option.logging is not None:
		logging.basicConfig(level=logging.__dict__[option.logging])
		logging.info('Logging level %s' % option.logging)


	# The action of the module as script should be concentrated
	# in the following function, to facilitate profiling.
	main(option, args)


	logging.info('Finishing')
	raise SystemExit


# \newpage \scriptsize
# \begin{multicols}{2}
# \section{History}
# \text{ }
#\\ $Log: animatebdi.pyw,v $
#\\ Revision 1.2  2008/07/14 13:53:11  wskendall
#\\ Cleaned up.
#\\
#\\ Revision 1.1  2008/07/05 15:25:46  wskendall
#\\ Initial revision
#\\
#\\ Revision 1.1  2008/07/05 10:33:33  wskendall
#\\ Initial revision
#\\
#
# \end{multicols}

'$Id: animatebdi.pyw,v 1.2 2008/07/14 13:53:11 wskendall Development wskendall $'
