#!/usr/bin/env python
# coding: utf-8

# Astronomical data analysis using Python
# ====================================
# 
# Lecture 3
# ---------------------
# 
# 

# Our first program - introduced in the previous lecture
# ------------------------------------------

# In[126]:


a = 3
b = 5
c = a+b
d = a-b
q, r = a/b, a%b # Yes, this is allowed!

# Now, let's print!
print ("Hello World!") # just for fun
print ("Sum, Difference = ", c, d)
print ("Quotient and Remainder = ", q, r)


# Our First Program - Rewritten!
# ------------------------------
# 
# Let us introduce the following modifications to the program.
# 
# * We use floats instead of ints.
# * We accept the numbers from the user instead of "hard coding" them.

# In[65]:


# Modified first program.
a = input("Please enter number 1: ")
b = input("Please enter number 2: ")

c, d = a+b, a-b
q, i = a/b, a//b

print (c,d,q,i)


# What happened?
# --------------
# 
# * Anything input through the keyboard using input() is ... a "string".
# * Strings support addition (concatenation) but nothing else.
# 
# So what should we do?
# ---------------
# 
# * "3.0" is a string. 3.0 is a float!
# * To convert "3.0" into a float, we use a simple function - float("3.0")
# 
# So, let's rewrite our program!

# In[127]:


a = float( input("Enter Number 1: ") ) # We are nesting functions here.
b = float( input("Enter Number 2: ") )

c,d = a+b, a-b
q,i = a/b, a//b # a//b is the floor division operator

print ("Addition = %f, Difference = %f " % (c,d))
print ("Quotient = %f, Floor division quotient  = %f" % (q,i))


# The output looks ugly. Wish I could control the number of decimal places...

# In[128]:


a = float( input("Enter Number 1: ") )
b = float( input("Enter Number 2: ") )

c,d = a+b, a-b
q,i = a/b, a//b

print("Addition = %.2f, Difference = %.2f " % (c,d))
print("Quotient = %.2f, Floor division quotient = %.2f" % (q,i))


# Ah! now, that's much better.

# String Formatting
# ----------
# 
# We have seen a powerful of constructing strings in the previous example.

# In[68]:


print ("Addition = %.2f, Difference = %.2f " % (c,d))


# C / FORTRAN users amongst you will immediately understand this method of string construction.
# 
# Python supports this and its own way of string formatting.
# -------

# In[69]:


gal_name = "NGC 7709"; int_bmagnitude = 13.6


# In[70]:


statement1 = "The galaxy %s has an integrated B-band magnitude of %.2f" % (gal_name, int_bmagnitude)


# In[71]:


statement2 = "The galaxy {0:s} has an integrated B-band magnitude of {1:.2f}".format(gal_name, int_bmagnitude)


# In[72]:


statement3 = "The galaxy {name:s} has an integrated B-band magnitude of {mag:.2f}".format(name=gal_name, mag=int_bmagnitude)


# All the above statements are equivalent!
# ------

# In[73]:


print (statement1,"\n", statement2, "\n", statement3, "\n")


# You can choose whichever method you like!
# 
# As a former C/C++ user, I tend to use the first method.
# 
# But ... second and third methods are more "Pythonic". If you don't have previous experience with the C type formatting use one of the more Pythonic ways.

# Raw Strings
# ------------
# 
# * We have seen the three methods of string declaration. 
# * We have also seen string formatting.
# * String formatting taught us that symbols like { or % have special meanings in Python.

# In[74]:


# There is a special way of declaring strings where
# we can ask Python to ignore all these symbols.
raw_string = r"Here is a percentage sign % and a brace }"
print (raw_string)


# Usefulness of Raw Strings - Example
# -------
# 
# * Typically, when we make plots and set labels, we would like to invoke a LaTeX parser.
# * This would involve a lot \ $ and {}. 
# 
# In such cases, it's best to use raw strings.
# 
#     plt.xlabel(r" \log \rm{F}_v")
#     
# Other examples with special characters include writing Windows file paths, XML code, HTML code, etc.

# Conditionals
# ------

# In[75]:


num = int(input("Enter number: ") )
if num %2 == 0:
    print ("%d is even!" % num)
else:
    print ("%d is odd!" % num)


# You will use conditionals a lot in your data analysis.

# In[1]:


model_choice = int(input( "Enter choice [1 or 2]: ") )
spectrum = 3 # In a realistic case, this will be some complicated object.

if model_choice == 1:
    #model1(spectrum)
    print ("Model 1 fitted.")
elif model_choice == 2:
    #model2(spectrum)
    print ("Model 2 fitted.")
else:
    print ("Invalid model entered.")


# What do you notice apart from the syntax in the above example?
# ------

# Indentation - A Vital Part of the Pythonic Way
# --------------
# 
# Be it the if-block illustrated above or the loops or the functions (to be introduced soon), indentation is at the heart of the Python's way of delimiting blocks!
# 
# Function definitions, loops, if-blocks - nothing has your typical boundaries like { } as in C/C++/Java.
# 
# The "level of the indentation" in the only way to define the scope of a "block".

# In support of indentation
# ------
# 
# Look at the following C-like code.
# 
#     if (x>0)
#         if (y>0)
#             print "Woohoo!"
#     else
#             print "Booboo!"
#             
# Which "if" does the "else" belong to?
# 
# In C and C-like languages, the braces {}s do the marking, the indentation is purely optional. In Python, indentation levels determine scopes. In Python the "the else" belongs to "if (x>0)". 
# 
# Python forces you to write clean code! (Obfuscation lovers, go take a jump!)
# 
# Use either spaces or tabs (don't ever ever mix them) and use them consistently. I strongly recommend using 4 spaces for each level of indentation.

# Wrapping up if-elif-else
# -------
# 
# The general syntax:
# 
#     if <condition>:
#         do this
#         and this
#     elif <condition>:
#         this
#         and this
#     ...
#     else:
#         do this 
#         and this
#      

# Conditions are anything that return True or False.
# ------
# 
# * == (equal to)
# * !=
# * \>
# * \>=
# * < 
# * <= 
# 
# You can combine conditionals using "logical operators"
# 
# * and
# * or
# * not

# The Boolean Data Type
# --------------

# In[5]:


a = True
b = False

if a:
    print ("This comes on screen.")

if b:
    print ("This won't come on screen.")
    
if a or b:
    print ("This also comes on screen.")
    
if a and b:
    print ("This won't come on screen either.")


# In[78]:


type(a) # To check type of object.


# Almost all other types have a Boolean Equivalent
# ------

# In[6]:


a = 1
b = 0

if a:
    print ("Hello!")
if b:
    print ("Oh No!")
    
type(a)    


# In[80]:


s1 = ""; s2 = "Hello" # s1 is an empty string

if s1:
    print ("Won't be printed.")
if s2:
    print ("Will be printed.")


# This is bad Python style because remember that explicit is better than implicit. Use an expression that evaluates to a boolean instead. Keep your programs readable.

# Conditional Expression
# -------
# 
# Consider...

# In[81]:


if 5 > 6:
    x = 2
else:
    x = 3


# In[82]:


y = 2 if 5 > 6 else 3 # if else block in one line is allowed


# In[83]:


print (x,y)


# A Second Plunge into the Data Types
# ===========
# 
# The two other data types we need to know:
# 
# * Lists 
# * Dictionaries
# 
# Data Types I will not cover (formally):
# 
# * Tuples (immutable lists!)
# * Sets (key-less dictionaries!)
# * Complex Numbers
# * Fractions
# * Decimals

# Lists
# -----

# In[84]:


a = [1,2,3,4] # simple ordered collection


# In[85]:


b = ["Hello", 45, 7.64, True] # can be heterogeneous


# In[86]:


a[0], a[-1], a[1:3] # All "sequence" operations supported.


# In[87]:


b[0][1] # 2nd member of the 1st member


# In[88]:


a = [ [1,2,3] , [4,5,6] , [7,8,9] ] # list of lists allowed.


# In[89]:


a[2][1] # Accessing elements in nested structures.


# In[90]:


[1,3,4] + [5,6,7] # Support concatenation


# In[91]:


[1,6,8] * 3 # Repetition (like strings)


# Lists are Mutable! (Strings are not!)
# ----

# In[9]:


a = [1,4,5,7]


# In[10]:


print (a)


# In[11]:


a[2] = 777 # set third element to 777


# In[12]:


print (a)


# List Methods
# ----

# In[13]:


a = [1,3,5]
print (a)


# In[97]:


a.append(7) # adds an element to the end
print (a) # the list has changed (unlike string methods!)


# In[98]:


a.extend([9,11,13]) # concatenates a list at the end
print (a)


# In[99]:


a.pop() # Removes one element at the end.
print (a)


# In[100]:


a.pop(2) # Removes 3rd element. 
print (a)


# Don't Forget!!!
# -----
# 

# In[101]:


print (dir(a)) # list of methods for a list "a"


# In[102]:


help(a.sort)


# Implications of Mutability
# ----

# In[103]:


l = [1,2,3,4]
m = l

l.append(5)
print (l)
print (m)


# l and m point to the same object. When the object mutates, whether you refer to it using l or m, you get the same mutated object.

# How do I make a copy then?
# ---

# In[104]:


l = [1,2,3,4]
m = l[:] 

l.append(5)
print (l)
print (m)


# Python has a module called "copy" available for making copies. Refer to the standard library documentation for details.

# Dictionaries
# ----
# 
# * Imagine a list as a collection of objects obj0, obj1, obj2 ... 
# * First object has a location 0, second 1 ... 
# * Now, imagine renaming location 0 as "something", location 1 as "somethingelse" ...
# * Earlier, you accessed objects at numbered locations a[0].
# * Now, you access objects by specifying location labels a["something"]
# 
# Let's see this at work.

# In[105]:


d1 = { "a" : 3, "b" : 5}
print (d1["a"])
print (d1["b"])


# "a", "b" are called keys and 3,5 are called values. So formally, a dictionary is a collection of key-value pairs.

# In[106]:


d1["c"] = 7 # Since "c" does not exist, a new key-value pair is made.
d1["a"] = 1 # Since "a" exists already, its value is modified.
print (d1) # the ordering of key-value pairs in the dictionary is not guaranteed.


# Dictionary Methods
# ----

# In[107]:


keys = d1.keys() # Returns a list of all keys which is stored in "keys".
print (keys)


# In[108]:


values = d1.values() # Returns a list of values.
print (values)


# In[109]:


d1.items() # List of Tuples of key-value pairs.


# Defining Dictionaries - ways to do this
# -----

# In[110]:


d1 = {"a":3, "b":5, "c":7} # we've seen this.


# In[111]:


keys =  ["a", "b", "c"]
values = [3,5,7]
d2 = dict( zip(keys,values) ) # creates dictionary similar to d1


# In[112]:


d3 = dict( a=3, b=5, c=7) # again, same as d1,d2


# In[14]:


d4 = dict( [ ("a",3), ("b",5), ("c",7) ] ) # same as d1,d2,d3


# Dictionaries in data analysis
# ===============
# 
# z['M31']
# 
# rmag['M1']
# 
# lumdist['3C 273']
# 
# 

# The while loop
# -------

# In[114]:


x = 0
while x<5:
    print (x)  
    x += 1


# In[115]:


x = 1
while True: # Without the break statement this loop is infinite
    print ("x = %d" % x)
    choice = input("Do you want to continue? ") 
    if choice != "y":
        break # This statement breaks the loop.
    else:
        x += 1
        


# The "for" loop - Pay Attention!
# -----
# 

# In[116]:


x = [5,6,7,8,9,0] # a simple list
for i in x:
    print (i)


# In " for i in x", x can be many different things, any iterable object is allowed.

# In[117]:


s = "Hello!"

for c in s:
    print (c)


# No No No! I want my good old for-loop back which generates numbers x to y in steps of z!!!

# In[118]:


# OKAY!!! Let's try something here.

for i in range(2,15,3):
    print (i)


# Let us see some wicked for-loops.

# In[17]:


a = [1,2,3,4,5]
b = "Hello"
c = zip(a,b)
print(type(c))

for i,j in c:
    print (i, j)


# In[133]:


a = "Hello!"

for i, c in enumerate(a):
    print ("Character no. %d is %s" % (i+1, c)) # i+1 is used because Python is 0-indexed.

print()
help(enumerate)


# You can break and continue for-loops too!

# In[18]:


for i in range(10000):
    if i%2 == 0: # Even
        print (i,"is Even")
        continue
    print (i, "is Odd")
    
    if i == 7: # What if I had said "i==8 or i==10" ??????
        break


# Traversing Dictionaries using for-loops
# ------

# In[136]:


d = dict( a = 1, b = 2, c = 3, d = 4)

for key,value in d.items():
    print (key, "-->", value)


# In[123]:


for key in d.keys():
    print (key, "-->", d[key])


# Style Guide for Python Code
# ----------
# 
# The PEP 8 provides excellent guidance on what to do and what to avoid while writing Python code. I strongly urge you to study PEP 8 carefully and implement its recommendations strictly.
# 
#  
# https://www.python.org/dev/peps/pep-0008/
# 
# The PEP 8 is very terse. For a more expanded explanation see:
# 
# 
# https://realpython.com/python-pep8/

# Python is fully Unicode UTF-8 standard compliant
# =====

# In[3]:


print("What is your name?")
print("আপনার নাম কি?")
print("உங்கள் பெயர் என்ன?")
print("तुझं नाव काय आहे?")
print("तुम्हारा नाम क्या हे?")


# Google provides the Cloud Translation API client library for Python 
# 
# https://cloud.google.com/translate/docs/reference/libraries/v2/python