Exercises

Targeted exercises

Importing libraries to add capabilities to Python

  1. In the IDLE or console of an IDE, import Numpy, and calculate the following. It may be helpful to know the following about Numpy: if you import Numpy as np then base-10 logarithms are accessed using np.log10, the value of $\pi$ is accessed using np.pi, the value of $e$ is accessed using np.e.

    • $\ln(1)$
    • $\ln(10)$
    • $\ln(e)$
    • $\log_{10}(1)$
    • $\log_{10}(10)$
    • $\log_{10}(e)$
    import numpy as np
    np.log(1)
    np.log(10)
    np.log(np.e)
    np.log10(10)
    np.log10(np.e)

  1. In this chapter we introduced log operations, but Numpy introduces more mathematical operations. Using the Numpy documentation, a web search, or an AI tool, figure out how to calculate the following using Numpy:

    • $-e^\pi$
    • $\sqrt{\pi}$
    • $\sin{\frac{\pi}{3}}$
    • Greatest common divisor of 275 and 385
    • Least common multiple of 12 and 27
    -1*np.exp(np.pi)
    np.sqrt(np.pi)
    np.sin(np.pi/3)
    np.gcd(275, 385)
    np.lcm(12, 27)

  1. Using Numpy, calculate the half life of a material with a decay rate of $1.3\times10^{-6} \text{ s}^{-1}$.
    np.log(2)/(1.3e-6)

Operating on collections of data efficiently with Numpy arrays

  1. Use a Numpy array to complete the following steps:

    • Make an array that holds the following pH values: 1.1, 2.2, 3.3, …, 9.9.
    • Use this array to calculate the concentration of protons at each p$H$.
    • Dilute all concentrations by a factor of 10.
    • Convert the diluted concentrations back to p$H$.
    pHs = np.array([1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9])
    print(pHs) # confirm answer
    
    proton_conc = 10**(-1*pHs)
    diluted_conc = proton_conc/10
    print(diluted_conc) # confirm answer
    
    -1*np.log10(diluted_conc)
    print(proton_conc) # confirm answer

  1. You wish to make a simple syrup that contains alcohol. You mix 220g of glucose with 220g of ethanol and then add enough water to bring the final volume to 0.75L. What is the concentration of the glucose and the ethanol? Use a Numpy array to complete this calculation.
    C = 12.01
    H = 1.008
    O = 16.00
    MWs = np.array([C*6 + H*12 + O*6, C + H*3 + C + H*2 + O + H]) # MW of glucose, alcohol
    mols = 220/MWs # mols of glucose, alcohol
    concs = mols/0.75 # concentration of glucose, alcohol
    print(concs)

  1. Numpy has many other functions, which are built to work with Numpy arrays. For instance, numpy.sum() can give you the sum of all elements in an array. Use this knowledge to find out the total concentration of solutes in Exercise 4.

    np.sum(concs)

  1. You are doing a variable temperature NMR experiment at 8 equally-spaced temperatures from -10$^\circ$C to 60$^\circ$C. You have two isomers that exchange and the $\Delta{G}$ of isomerization is $10kJ/mol$. For each species, calculate the concentration/relative population at each temperature.

    R = 8.31446261815324e-3
    temps_C = np.linspace(-10, 60, 8)
    temps_K = temps_C + 273
    Keq = np.exp(-10/(R*temps_K)) # this is the relative concentrations
    print(Keq)

Interpreting Python error messages to fix mistakes

  1. Write code that, when run, generates an error of each type below. You might wish to consult Chapter the chapter on errors or the internet.

    • Errors in variable naming.
    • Errors in making an array.
    • Adding vectors of different shape.
    • Using a function incorrectly.
    • Syntax errors.
    • Indentation error.
    • Forgotten `:'.
    2a = "aa"
    np.array(1, 3, 5)
    np.array([1, 3, 5]) + np.array([1, 2])
    rounded = np.round(2.1, decimal = 1)
    a + 1 = 3
    a = "a"
    	b = "b"
    def test()

Structuring data using arrays of arrays

  1. Make a $3\times3$ Numpy 2D array, in which each index holds a different number from 1–9:

    • Make the numbers count up by one as you go across the “rows.”
    • Make the numbers count up by one as you go down the “columns.”
    np.array([
    	[1,2,3],
    	[4,5,6],
    	[7,8,9]
    	])
    np.array([
    	[1, 4, 7],
    	[2, 5, 8],
    	[3, 6, 9]
    ])

  1. Imagine you had 4 solutions that had concentrations of 1.0M, 0.70M, 0.55M, and 0.13M.

    • Make a Numpy array that holds these concentrations.
    • Create a 2D array that holds three sets of arrays that would represent serial dilution of these concentrations by a factor of 3, performed twice.
    • Subtract the array you just made from a 2D array that represents 12 solutions all of 1.00M concentration.
    concentrations = np.array([1.0, 0.70, 0.55, 0.13])
    serial_dilutions = np.array([
    	concentrations,
    	concentrations/3,
    	concentrations/9
    	])
    print(serial_dilutions)
    print(np.array([[1,1,1,1],[1,1,1,1],[1,1,1,1]]) - serial_dilutions)

Generating arrays of all ones or zeros Numpy

  1. Make a 15 element 2D Numpy array where all elements have a value of 0.33.

    • Make this array, using numpy.ones()
    • Make this array, using numpy.ones_like()
    • Make this array, using numpy.zeros()
    • Make this array, using numpy.zeros_like()
    first_array = np.ones([3,5])*0.33
    print(first_array)
    np.ones_like(first_array)*0.33
    np.zeros([3,5])+0.33
    np.zeros_like(first_array)+first_array

Generating an array of equally spaced numbers using Numpy

  1. Create the following Numpy arrays. You cannot just type out the values to create the array, you must use at least numpy.linspace() or numpy.arange() in your solution:

    • An array from 0 to 10, not including 10, with 10 elements, using numpy.arange().
    • An array from 0 to 10, not including 10, with 10 elements, using numpy.linspace().
    • An array spanning 0 to 10, including 10, with 10 elements.
    • An array spanning 0 to 10, including 10, with each element differing by 0.5.
    • An array from 0 to $2\pi$, not including $2\pi$ with 8 elements.
    • Make an array that, when squared, has elements 0 to 10, equally spaced.
    • Using numpy.linspace() and numpy.sin(), make an array that has the values $[0, \sqrt{2}, 1, \sqrt{2}, 0, -\sqrt{2}, -1, -\sqrt{2}]$.
    • An array spanning 1 to $10^{14}$ (including $10^{14}$), where each element is 10 times larger than the element before it.
    np.arange(0,10,1)
    np.linspace(0,9,10)
    np.linspace(0,10,10)
    np.arange(0,10.5,0.5)
    np.linspace(0, 2*np.pi, 8)
    np.sqrt(np.linspace(0, 10, 8))
    np.sin(np.linspace(0, 7/4*np.pi, 8))
    (np.ones(15)*10)**np.linspace(0,14,15)

Planning a multi-step solution to a complex problem

  1. You have created a series of molecules that you think will serve as p$H$ sensors, and you need to calibrate their responses to changes in p$H$. Write a code that will specify how much of a 6N HCl solution to use for 11 standards, each 10 mL in volume and which area equally spaced between two p$H$ that the user of the code can specify.

    def calc_vols (start_pH, stop_pH):
    	pHs = np.linspace(start_pH, stop_pH, 11)
    	H_conc = np.exp(-pHs)
    	vols = H_conc*0.01/6 # concentration times 10mL / molarity of HCl
    	return vols
    
    print(calc_vols(3, 7))

  1. Draw a flow chart for the function produced at the end of Chapter 0.

    Pasted image 20250327204731.png Pasted image 20250327204731.png


  1. Create a flow chart for how you might solve an ICE table in general chemistry course.

    Pasted image 20250327204935.png Pasted image 20250327204935.png


Writing scripts to execute complete solutions in one click

  1. Create a file containing the solution to Exercise 2 and save it to a directory on your computer. Your Script should be in a file—perhaps named “half-life.py” and should read:
    np.log(2)/(1.3e-6)

Keeping track of changes to your code using versioning practices

  1. You wrote a Python script. Y start at version 0.1.0 and go through the changes described below, in turn. Next to each entry, provide the version number that should be assigned to your script after the changes.
    • You add a new keyword_argument to a function, with a default value that was originally hard coded.

    • You notice a typo in a comment, and fix it

    • You notice a typo in a function that breaks the function, and fix it.

    • You add a new positional_argument (that does not have a default value) to a function.

    • You notice an mathematical error in a formula, and correct it.

    • You decide to reorder the structure of your code.

    • You have your code output additional text contextualizing results that are printed to the console.

    • Since the default value of the new keyword argument is the same as the original hard coded value, this is not a breaking change. However, it is not simply a bug fix, but adds new capabilities. We can assign this as 0.2.0.

    • This is a simple bug fix, that doesn’t even change anything about how the program works. You can increment to 0.2.1

    • This is almost the definition of a bug fix. increment to 0.2.2

    • Because this positional argument does not have a default value, it will break the functionality of previous versions. Increment to 1.0.0

    • This would be a bug fix. Increment to 1.0.1.

    • This does not change behavior at all, so it is similar to a bug fix. 1.0.2

    • This is a minor change that doesn’t add a capability, but just clarifies an existing one. A case can be made for either 1.0.3, or 1.1.0.


Comprehensive exercises

  1. Take the final code from this chapter and adapt it for a 384 well plate. You should only need to change two numbers to do this. Try to ensure that the printed result is easy to follow.

    # Version 0.2.0 - 240605 - Took the function and wrote a script to calculate volumes of stock solutions needed for a well plate of arbitrary size.
    # Version 0.1.0 - 240605 - A function that calculates volumes of stock solutions needed for a well in a well plate. 
    
    import numpy as np
    
    # A function to calculate the volumes of stock solutions needed to create solutions with given catalyst, HCl, and ionic strength
    # All concentrations are in M
    # The final volume is in mL and assumed to be 1 unless provided
    
    def calcPlateVols(conc_cat, conc_HCl, I, vol_final = 1):
    
        # Define our stock solutions
        conc_stock_cat = 0.1 # M
        conc_stock_HCl = 6 # M
        conc_stock_NaCl = 3 # M
    
        # Calculate the volumes needed
        vol_cat = conc_cat / conc_stock_cat * vol_final
        vol_HCl = conc_HCl / conc_stock_HCl * vol_final
        vol_NaCl = (I - conc_HCl) / conc_stock_NaCl * vol_final
    
        # Calculate the water needed to make up 1 mL
        vol_water = vol_final - vol_cat - vol_HCl - vol_NaCl
    
        print('[ ] catalyst solution (mL)\n', vol_cat)
        print('[ ] HCl (mL)\n', vol_HCl)
        print('[ ] NaCl (mL)\n', vol_NaCl)
        print('[ ] water (mL)\n', vol_water)
    
    # Define the dimensions of our well plate
    rows = 16
    cols = 24
    
    # Define our experimental concentrations and ionic strengths
    conc_cat = 0.01 # M
    conc_HCl_start = 0.0 # M
    conc_HCl_end = 0.01 # M
    I_start = 0.02 # M
    I_end = 0.2 # M
    
    # Get the concentration of catalyst in each well
    cat = conc_cat * np.ones((rows, cols))
    
    # Get the concentration of HCl in each well
    # We will do this by multiplying two 1D arrays to make a 2D array
    # First, define the concentration of HCl in each row
    MHCl_row = np.linspace(conc_HCl_start, conc_HCl_end, cols)
    
    # Next, each row should be the same, so make an array of all ones
    MHCl_col = np.ones(rows)
    
    # Finally, we can outer multiply these two arrays to make a 2D array
    MHCl = np.outer(MHCl_col, MHCl_row)
    
    # Now we need to get the ionic strengths in each well by a similar method
    # Here, the columns are all the same instead of the rows
    # First, make a row that is all ones
    ionic_row = np.ones(cols)
    
    # Next, make an array that represents one column
    ionic_col = np.linspace(I_start, I_end, rows)
    
    # Finally, outer multiply to get the 2D array
    ionic = np.outer(ionic_col, ionic_row)
    
    # Calculate and print the volumes
    calcPlateVols(cat, MHCl, ionic)

    If you print each of these out on a large enough piece of paper (or small enough font) you can get them to have each row span the paper. Then you can just check off the ones you have added.


  1. You find that your pipette is only accurate to the nearest $0.1\mu$L. Modify the final code in this chapter so that it prints out volumes rounded to the nearest $0.1\mu$L. Looking at your answer, could you carry out this experiment if you had a pipet that was only accurate to $1\mu$L? How do you know?

    # Version 0.2.0 - 240605 - Took the function and wrote a script to calculate volumes of stock solutions needed for a well plate of arbitrary size.
    # Version 0.1.0 - 240605 - A function that calculates volumes of stock solutions needed for a well in a well plate. 
    
    import numpy as np
    
    # A function to calculate the volumes of stock solutions needed to create solutions with given catalyst, HCl, and ionic strength
    # All concentrations are in M
    # The final volume is in mL and assumed to be 1 unless provided
    
    def calcPlateVols(conc_cat, conc_HCl, I, vol_final = 1):
    
        # Define our stock solutions
        conc_stock_cat = 0.1 # M
        conc_stock_HCl = 6 # M
        conc_stock_NaCl = 3 # M
    
        # Calculate the volumes needed
        vol_cat = np.round(conc_cat / conc_stock_cat * vol_final, 4)
        vol_HCl = np.round(conc_HCl / conc_stock_HCl * vol_final, 4)
        vol_NaCl = np.round((I - conc_HCl) / conc_stock_NaCl * vol_final, 4)
    
        # Calculate the water needed to make up 1 mL
        vol_water = vol_final - vol_cat - vol_HCl - vol_NaCl
    
        print('[ ] catalyst solution (mL)\n', vol_cat)
        print('[ ] HCl (mL)\n', vol_HCl)
        print('[ ] NaCl (mL)\n', vol_NaCl)
        print('[ ] water (mL)\n', vol_water)
    
    # Define the dimensions of our well plate
    rows = 4
    cols = 6
    
    # Define our experimental concentrations and ionic strengths
    conc_cat = 0.01 # M
    conc_HCl_start = 0.0 # M
    conc_HCl_end = 0.01 # M
    I_start = 0.02 # M
    I_end = 0.2 # M
    
    # Get the concentration of catalyst in each well
    cat = conc_cat * np.ones((rows, cols))
    
    # Get the concentration of HCl in each well
    # We will do this by multiplying two 1D arrays to make a 2D array
    # First, define the concentration of HCl in each row
    MHCl_row = np.linspace(conc_HCl_start, conc_HCl_end, cols)
    
    # Next, each row should be the same, so make an array of all ones
    MHCl_col = np.ones(rows)
    
    # Finally, we can outer multiply these two arrays to make a 2D array
    MHCl = np.outer(MHCl_col, MHCl_row)
    
    # Now we need to get the ionic strengths in each well by a similar method
    # Here, the columns are all the same instead of the rows
    # First, make a row that is all ones
    ionic_row = np.ones(cols)
    
    # Next, make an array that represents one column
    ionic_col = np.linspace(I_start, I_end, rows)
    
    # Finally, outer multiply to get the 2D array
    ionic = np.outer(ionic_col, ionic_row)
    
    # Calculate and print the volumes
    calcPlateVols(cat, MHCl, ionic)

    _Looking at the solutions, you can see that the volume of added $\ce{HCl}$ is 0.0003, 0.0007, 0.001, 0.0013 mL. If you had to round to 1$\mu$L, then these values would be 0.000, 0.001, 0.001, and 0.001. Therefore, this experiment could not be done. _


  1. Your advisor decides that they want you to add a small amount of another solvent to your reaction mixture. Modify the final code in the chapter to allow for the same arbitrary amount of that solvent to be added to each well.

    # Version 0.2.0 - 240605 - Took the function and wrote a script to calculate volumes of stock solutions needed for a well plate of arbitrary size.
    # Version 0.1.0 - 240605 - A function that calculates volumes of stock solutions needed for a well in a well plate. 
    
    import numpy as np
    
    # A function to calculate the volumes of stock solutions needed to create solutions with given catalyst, HCl, and ionic strength
    # All concentrations are in M
    # The final volume is in mL and assumed to be 1 unless provided
    
    def calcPlateVols(conc_cat, conc_HCl, I, sol_vol, vol_final = 1):
    
        # Define our stock solutions
        conc_stock_cat = 0.1 # M
        conc_stock_HCl = 6 # M
        conc_stock_NaCl = 3 # M
    
        # Calculate the volumes needed
        vol_cat = conc_cat / conc_stock_cat * vol_final
        vol_HCl = conc_HCl / conc_stock_HCl * vol_final
        vol_NaCl = (I - conc_HCl) / conc_stock_NaCl * vol_final
    
        # new addition for solvent
        vol_sol = np.ones_like(vol_NaCl)*sol_vol
    
        # Calculate the water needed to make up 1 mL
        vol_water = vol_final - vol_cat - vol_HCl - vol_NaCl - sol_vol
    
        print('[ ] catalyst solution (mL)\n', vol_cat)
        print('[ ] HCl (mL)\n', vol_HCl)
        print('[ ] NaCl (mL)\n', vol_NaCl)
        print('[ ] Solvent (mL)\n', vol_sol )
        print('[ ] water (mL)\n', vol_water)
    
    # Define the dimensions of our well plate
    rows = 4
    cols = 6
    
    # Define our experimental concentrations and ionic strengths
    conc_cat = 0.01 # M
    conc_HCl_start = 0.0 # M
    conc_HCl_end = 0.01 # M
    I_start = 0.02 # M
    I_end = 0.2 # M
    sol_vol = 0.002
    
    # Get the concentration of catalyst in each well
    cat = conc_cat * np.ones((rows, cols))
    
    # Get the concentration of HCl in each well
    # We will do this by multiplying two 1D arrays to make a 2D array
    # First, define the concentration of HCl in each row
    MHCl_row = np.linspace(conc_HCl_start, conc_HCl_end, cols)
    
    # Next, each row should be the same, so make an array of all ones
    MHCl_col = np.ones(rows)
    
    # Finally, we can outer multiply these two arrays to make a 2D array
    MHCl = np.outer(MHCl_col, MHCl_row)
    
    # Now we need to get the ionic strengths in each well by a similar method
    # Here, the columns are all the same instead of the rows
    # First, make a row that is all ones
    ionic_row = np.ones(cols)
    
    # Next, make an array that represents one column
    ionic_col = np.linspace(I_start, I_end, rows)
    
    # Finally, outer multiply to get the 2D array
    ionic = np.outer(ionic_col, ionic_row)
    
    # Calculate and print the volumes
    calcPlateVols(cat, MHCl, ionic, sol_vol)