Practical 2
Part 1 - Exploring Python
Data Types
A given data type can be as simple as a single digit, or as complex as a suite of genome databases. Python comes with a number of relatively simple (but still extremely useful) data-types, and ScrumPy extends this by providing additional data-types particularly useful for metabolic modelling and related activities.
We will start here by examining some of the common built-in types.
Strings
Strings are collections of characters. Characters in a string can be accessed by indexing, and membership of a subset of characters in a string can be evaluated.
Numerical types
The numerical types we will be dealing with are integers, int, and floating-point numbers, float. Integers are written as a sequence of digits. Floats are written as digits with a decimal point in the sequence, and an optional exponent (e or E).
The type of a given data object can be checked using the built-in function type().
Floats and integers can be interconverted using the constructors int() or float().
The common mathematical operators (+,-,/,*) work as expected, note that x**y means xy.
Boolean
Booleans are a subtype of integers. A boolean type is either True or False, and can be very useful when writing conditional statements, i.e. if something is True, do something. Also, the integer 0 is False.
Lists (and tuples)
Lists and tuples are collections of items in which are stored in a specific order and each item is associated with (indexed by) an integer. The main difference between the two is that tuples are immutable - once a tuple is created it cannot be changed, whereas lists can. For these exercises we will mainly use lists. An empty list can be created by assigning a pair of closed square brackets to a variable.
1 >>> empty_list=[]
Items can be appended to a list by using the append() method.
A list can also be created and populated in one go.
Items can be removed from a list using the remove() method.
As with strings, indexing can be used to copy a subset of a list, keep in mind that the indices of items in lists (like characters in strings) are numbered from 0. Membership of an item in a list can be evaluated as described for strings.
Subsets of lists (and strings) can be accessed using slicing:
The index of a known item can be retrieved using the index() method.
Lists can contain any objects, including other lists. These lists are referred to as nested:
Dictionaries
In other programming languages dictionaries are sometimes called "associative arrays". Unlike lists, dictionaries store collections of items that are ordered by keys, not indices. There is no specific order of the items in a dictionary. The keys of a dictionary must be unique (for a given dictionary) and be hashable, for now this means that any object that is not a list can be used as a key. Here are some examples of dictionaries in action:
1 >>> dict_1 = {'alfa':1,'beta':2} #create a dictionary
2 >>> keys = ['alfa','beta']
3 >>> vals = [1,2]
4 >>> dict_1 = dict(zip(keys,vals)) #create a dictionary from two lists
5 >>> dict_1
6 {'alfa':1,'beta':2}
7 >>> dict_1['alfa'] #access value '1' by key 'alfa'
8 1
9 >>> dict_1.has_key('beta') #check that dict_1 has key 'beta'
10 True
11 >>> dict_1.keys() #print keys of dict_1
12 ['alfa','beta']
13 >>> dict_1.values() #print values
14 [1,2]
15 >>> dict_1['gamma'] = 1 #add new key:value pair
16 >>> dict_1['alfa'] = 'a' #overwrite key:value pair
As with lists, dictionaries can contain nested dictionaries:
1 >>> dict_1 = {'alfa':1,'beta':2} #define first dictionary
2 >>> dict_2 = {'gamma':1} #define second dictionary
3 >>> dict_1['nested'] = dict_2 #add dict_2 as value to key 'nested' in dict_1
4 >>> dict_1
5 {'beta': 2, 'alfa': 1, 'nested': {'gamma': 1}}
6 >>> dict_1['nested']['gamma'] #access key 'gamma' in nested dictionary
7 1
Modules and functions
user@machine:~$ Assume a file called "some_py_func.py" contains the following:
#print user-specified string
def print_string(string):
print string 1 >>> import some_py_func #import some_py_func.py (note absence of extension!)
It is usually more convenient to structure the stored code into functions that can be executed from the imported module. Functions are defined using the key-word def.
In many applications, functions return objects to the user, as in the following version of print_string in some_py_func.py:
user@machine:~$ more some_py_func.py
#return list of characters in user-specified string
def print_string(string):
return list(string)Modify some_py_func.py to contain an additional function print_hello as follows:
#return list of characters in user-specified string
def print_string(string):
return list(string)
def print_hello():
print 'Hello world!'Now we can call a second function from the same module:
Functions don't have to be defined within modules; defining them in the shell also works:
Functions often require arguments that the user is supposed to provide.
user@machine:~$ more some_py_func.py
#return list of characters in user-specified string
def print_string(string):
return list(string)
def print_hello():
print 'Hello world!'
Objects
As mentioned, Python supports multiple programming paradigms, one of those being object-orientation. Object-orientation allows collection of data into objects, or class instances. The data collected in objectes is referred to as fields or attributes, objects also store functions that usually performs actions on the attributes. Object-specific functions are called methods. Here is a small example of class definition, initialisation, and usage of a class that stores a list and a dictionary.
user@machine:~$ more StringDL.py
class StringDnLs:
def __init__(self):
self.str_list=[]
self.str_dict={}
def add2List(self,item):
self.str_list.append(item)
def add2Dict(self,key,val):
self.str_dict[key]=val 1 >>> import StringDL #import module
2 >>> strdn = StringDL.StringDnLs() #create instance of class StringDnLs
3 >>> strdn.str_list #print StringDnLs field str_list
4 [] # which is empty
5 >>> strdn.add2List('a string') # add a string to str_list using method addList(item)
6 >>> strdn.str_list
7 ['a string']
Built-in functions, loops, conditionals, assignment, and evaluation
Some of the conventions for Python syntax we have already seen. Useful built-in functions include len(), which returns the length of an object,
and dir(), which returns a list of methods and attributes of an object.
You can read more about the built-in functions here.
range(integer_1,integer_2) is a built-in function that returns a list of integers ranging from the one integer, integer_1, to (but excluding) the other, integer_2. If the first argument is left blank, Python assumes integer_1 is 0. For example:
The step size is 1 by default, but can be specified as the third argument:
Note that the step size must be an integer. If floating point steps are needed, the arange() function from the numpy package can be used. It is very similar to the range() function but accepts floating point arguments and returns array objects, which can be converted to lists using the method tolist()
The for loop is used to iterate over an iterable object, e.g. a list. Depending on how the loop is formulated the loop variable will either be an item in the iterable object or an index.
while loops iterate until a condition is fulfilled.
1 >>> a_list = ['a','b','c']
2 >>> i = 0 #assign value 0 to variable i
3 >>> while i<len(a_list): #as long as i is less than 3
4 print a_list[i] #print item at index i in a_list
5 i += 1 #increment i by 1
6
7 a
8 b
9 c
10
11
12
13 >>> for i in range(len(a_list)): #iterating over indices
14 print a_list[i]
15
16 a
17 b
18 c
This implies that if the condition i<len(a_list) is never fulfilled the loop continues indefinitely, which it will.
Loops can be combined with conditional statements, where a block of code is executed if a statement is true, else another block is executed. The else block is optional, but must be the last option and no statement may follow on the same line.
If several options are possible the elif statement can be used.
You may have noticed it already, but it is necessary to point out the distinction between assignment and evaluation:
Part 2 - Creating Models
Loading and defining models
A pre-existing model is loaded by creating an instance of a model object ScrumPy.Model(model) with model being the name of the file (string-type ending with the ScrumPy file extension, .spy, all enclosed in quotes) containing the description of the model, provided that the file is stored in the directory from where ScrumPy was launched. If no name is provided, a GUI for selecting a model to open, is launched.
1 >>> m = ScrumPy.Model('toy_model.spy')
If this file has not yet been created, the instruction will open a new empty editor window; copy and paste the model description below. Having done that, select "compile" from the ScrumPy menu in the editor, and the model is loaded. toy_model.spy is the small structural model described by:
Structural()
A_tx:
x_A -> A
~
R_1:
A -> B
~
R_2:
B -> C
~
R_3:
C -> E
~
R_4:
B -> D
~
R_5:
D -> E
~
R_6:
D -> F
~
E_tx:
E -> x_E
~The reactions with the suffix "_tx" are transporters, i.e. they convert external metabolites (with prefix "x_"), which can be consumed or produced, to internal metabolites, which have no net consumption or production at steady state.
Properties of structural models
The class Model has a range of methods, of which some are only useful for kinetic models (which are also structural models, but the opposite is not true). The methods are actions on the model object (m in this case), and are invoked by instructions such as x = m.Method(), where the outcome is a new data object x that contains the result. Among the structurally relevant methods we find ConsMoieties() - which returns a list of conserved moieties; DeadReactions() - which returns a list of reactions that cannot carry steady state flux; FindIsoforms() - which identifies reactions from the model that are redundant, i.e. a set of reactions that have identical stoichiometry; ElModes() - which returns an elementary modes object; Externals() - which returns a list of external metabolites.
The stoichiometry matrix
The fields Model.sm and Model.smx are the two stoichiometry matrices associated with a model - the former is the internal matrix, the latter the external. The external matrix contains infomation about external metabolites, whereas the internal does not. All instances of ScrumPy matrices (subclasses of DynMatrix) have the fields cnames - column names and rnames - row names.
Useful methods of sm (and smx) include ReacToStr(reac),
and InvolvedWith(name),
Reaction reversibility
ScrumPy accepts three reversibility symbols: "->" - left to right irreversible, "<-" - right to left irreversible, and <> - reversible. Reaction reversibility is handled by the stoichiometry matrix.
Nullspace analysis
The kernel of the stoichiometry matrix can be calculated using the sm.NullSpace() method (smx also has the method, but the kernel of the external matrix is only related to the futile cycles of the model, not possible steady-state solutions that involve consumption of externals).
Even if the signs of some of the coefficients indicate thermodynamically infeasible solutions (e.g. all active reactions in the first column have negative coefficients, even though they are irreversible) a lot of useful information can be obtained form k. For instance, we see that the row associated with R_6 is a null-vector, indicating that there is no steady-state solution involving R_6. In fact this is how ScrumPy detects dead reactions with the method DeadReactions().
Enzymes subsets
Also, note that some of the row-vectors are proportional to each other - R_2, R_3; R_4, R_5; and E_tx, A_tx, R_1. This implies that these sets must carry flux in a coordinated fashion, e.g. any flux solution involving R_4 must also involve R_5. These sets of coordinated reactions are referred to as enzyme subsets and can be determined using the Model method EnzSubsets(). This method returns a dictionary object where keys are subset names (or reaction name if a reaction is in a singleton set) and values are nested dictionaries where keys are reaction names and values are the flux ratios of the reactions. The key DeadReacs maps to a list of dead reactions.
Elementary modes
The elementary modes of a model can be analysed using the method Model.ElModes(). This method returns an object that collects methods and fields related to the elementary modes of a given model.
The field mo is a matrix similar to k.
The relationship between modes and metabolites is stored in the sto matrix.
The methods Modes() and Stos() returns a string with with same information as the matrices above.
(a) Create and load toy_model.spy as described in the tutorial.
(b) See what happens if you remove the tilde sign (~) from any of the reactions in the model file. Implement the modification either by selecting ScrumPy > Compile in the model file, or do:
1 >>> m.Reload()
in the ScrumPy window (after saving the modification in the model file).
(c) Modify the reaction stoichiometry in any of the reactions in toy_model.spy so that all reactants are identical, e.g.:
Reload or compile the model as above. What happens?
Part 3 - The Stoichiometry Matrix and Null-space
(a) Familiarise yourself with the stoichiometry matrix of the toy model, e.g. repeat the examples in the section of the tutorial relating to the stoichiometry matrix.
(b) Generate a kernel of the stoichiometry matrix (using the .NullSpace() method).
(c) Multiply the stoichiometry matrix by the kernel matrix, using the .Mul() method of the stoichiometry matrix.
(d) Repeat the multiplication above using the external stoichiometry matrix instead.
(e) Create a kernel of the external stoichiometry matrix - what is different?
(f) Multiply the internal and external stoichiometry matrices by your new kernel.
(g) Try adding or removing reactions from the model, and repeat the exercises above, explain the new results.