Chapter 1

Chapter 1

Table of Contents

Data

There is no data required for or used in Chapter 1.

Code blocks from chapter

# 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 = 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)

Solutions to Exercises

Targeted exercises

Importing libraries to add capabilities to Python

Exercise 0

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)

Exercise 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)

Exercise 2

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

Exercise 3

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 pH.
  • Dilute all concentrations by a factor of 10.
  • Convert the diluted concentrations back to pH.
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

Exercise 4

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)

Exercise 5

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)

Exercise 6

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

Exercise 7

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

Exercise 8

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]
])

Exercise 9

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

Exercise 10

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

Exercise 11

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

Exercise 12

You have created a series of molecules that you think will serve as pH sensors, and you need to calibrate their responses to changes in pH. 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 pH 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))

Exercise 13

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


Exercise 14

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

Exercise 15

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

Exercise 16

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

Exercise 17

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.

Exercise 18

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. _

Exercise 19

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)