This tool facilitates determining yeast cell density of a culture if provided the absorbance at 660 nm, a.k.a. optical density at 660 nm, and the dilution factor of the sample.
This tool is based on the approach described in the Methods in Yeast Gentics Appendix entitled "Measuring Yeast Cell Density by Spectrophotometry".
If you haven't used one of these notebooks before, they're basically web pages in which you can write, edit, and run live code. They're meant to encourage experimentation, so don't feel nervous. Just try running a few cells and see what happens!.
Some tips:
To keep in mind when running via MyBinder:
Below enter the absorbance at 660 nm (O.D. 660) of the culture or diluted culture sample.
absorbance = 1.323
Below enter the dilution factor of the sample.
Enter below the dilution_factor as
dilution_factor = "none"
, if the sample is undiluted culture.
(You may have needed to dilute the sample to get the reading to be around 1.0 Absorbance unit or less, which is typically the best range for most spectrohphotometers.)
Dilution factor, here, is the number do you need to multiply the original culture volume to get the final volume, see here. In other words, dilution_factor equals the fold dilution.
If it is a ten-fold dilution simply enter dilution_factor = 10
Note that quotes are not involved if the dilution factor is numerical and not none
.
dilution_factor = "none"
Below set the haploid
setting to False
if you are working with a diploid strain.
haploid = True
## PREPARATION AND HELPER FUNCTIONS FOR THE CODE ##
import numpy
# *****Standard Curve *********
# *****************************
# List of tuples of OD660 vs haploid number of cells per ml. First values is the
# OD660 readings and 2nd are the number of haploid cells in the mL sample, as
# provided for strain A364A. Cell densities for diploids are half those for
# halploids as diploids and some mutants which are abnormally large will
# scatter more light than the wildtype haploids at the same cell density.
yeast_cell_density_by_OD660_tuples = [
(0.000, 0.000e7),
(0.010, 0.015e7),
(0.020, 0.025e7),
(0.030, 0.040e7),
(0.040, 0.053e7),
(0.050, 0.065e7),
(0.060, 0.078e7),
(0.070, 0.090e7),
(0.080, 0.103e7),
(0.090, 0.115e7),
(0.100, 0.128e7),
(0.110, 0.140e7),
(0.120, 0.153e7),
(0.130, 0.165e7),
(0.140, 0.178e7),
(0.150, 0.190e7),
(0.160, 0.204e7),
(0.170, 0.216e7),
(0.180, 0.229e7),
(0.190, 0.241e7),
(0.200, 0.255e7),
(0.210, 0.268e7),
(0.220, 0.280e7),
(0.230, 0.293e7),
(0.240, 0.305e7),
(0.250, 0.319e7),
(0.260, 0.330e7),
(0.270, 0.342e7),
(0.280, 0.356e7),
(0.290, 0.370e7),
(0.300, 0.385e7),
(0.310, 0.399e7),
(0.320, 0.412e7),
(0.330, 0.426e7),
(0.340, 0.440e7),
(0.350, 0.455e7),
(0.360, 0.470e7),
(0.370, 0.484e7),
(0.380, 0.499e7),
(0.390, 0.514e7),
(0.400, 0.530e7),
(0.410, 0.547e7),
(0.420, 0.564e7),
(0.430, 0.580e7),
(0.440, 0.600e7),
(0.450, 0.617e7),
(0.460, 0.633e7),
(0.470, 0.650e7),
(0.480, 0.666e7),
(0.490, 0.683e7),
(0.500, 0.700e7),
(0.510, 0.717e7),
(0.520, 0.733e7),
(0.530, 0.750e7),
(0.540, 0.766e7),
(0.550, 0.783e7),
(0.560, 0.800e7),
(0.570, 0.817e7),
(0.580, 0.833e7),
(0.590, 0.850e7),
(0.600, 0.866e7),
(0.610, 0.883e7),
(0.620, 0.900e7),
(0.630, 0.917e7),
(0.640, 0.933e7),
(0.650, 0.950e7),
(0.660, 0.966e7),
(0.670, 0.983e7),
(0.680, 1.000e7),
(0.690, 1.023e7),
(0.700, 1.046e7),
(0.710, 1.070e7),
(0.720, 1.093e7),
(0.730, 1.116e7),
(0.740, 1.140e7),
(0.750, 1.160e7),
(0.760, 1.180e7),
(0.770, 1.200e7),
(0.780, 1.220e7),
(0.790, 1.240e7),
(0.800, 1.260e7),
(0.810, 1.283e7),
(0.820, 1.306e7),
(0.830, 1.330e7),
(0.840, 1.353e7),
(0.850, 1.376e7),
(0.860, 1.400e7),
(0.870, 1.430e7),
(0.880, 1.460e7),
(0.890, 1.490e7),
(0.900, 1.520e7),
(0.910, 1.550e7),
(0.920, 1.580e7),
(0.930, 1.610e7),
(0.940, 1.640e7),
(0.950, 1.670e7),
(0.960, 1.703e7),
(0.970, 1.736e7),
(0.980, 1.770e7),
(0.990, 1.810e7),
(1.000, 1.850e7),
(1.010, 1.890e7),
(1.020, 1.926e7),
(1.030, 1.963e7),
(1.040, 2.000e7),
(1.050, 2.040e7),
(1.060, 2.080e7),
(1.070, 2.120e7),
(1.080, 2.163e7),
(1.090, 2.206e7),
(1.100, 2.250e7),
(1.110, 2.296e7),
(1.120, 2.343e7),
(1.130, 2.390e7),
(1.140, 2.433e7),
(1.150, 2.476e7),
(1.160, 2.520e7),
(1.170, 2.566e7),
(1.180, 2.613e7),
(1.190, 2.660e7),
(1.200, 2.706e7),
(1.210, 2.753e7),
(1.220, 2.800e7),
(1.230, 2.850e7),
(1.240, 2.900e7),
(1.250, 2.950e7),
(1.260, 3.002e7),
(1.270, 3.055e7),
(1.280, 3.107e7),
(1.290, 3.160e7),
(1.300, 3.220e7),
(1.310, 3.280e7),
(1.320, 3.340e7),
(1.330, 3.400e7),
(1.340, 3.460e7),
(1.350, 3.520e7),
(1.360, 3.580e7),
(1.370, 3.640e7),
(1.380, 3.700e7),
(1.390, 3.760e7),
(1.400, 3.820e7),
(1.410, 3.880e7),
(1.420, 3.940e7),
(1.430, 4.000e7),
(1.440, 4.065e7),
(1.450, 4.130e7),
(1.460, 4.200e7),
(1.470, 4.270e7),
(1.480, 4.340e7),
(1.490, 4.410e7),
(1.500, 4.480e7),
(1.510, 4.550e7),
(1.520, 4.625e7),
(1.530, 4.700e7),
(1.540, 4.775e7),
(1.550, 4.850e7),
(1.560, 4.925e7),
(1.570, 5.000e7),
(1.580, 5.075e7),
(1.590, 5.150e7),
(1.600, 5.225e7),
(1.610, 5.300e7),
(1.620, 5.380e7),
(1.630, 5.460e7),
(1.640, 5.540e7),
(1.650, 5.630e7),
(1.660, 5.700e7),
(1.670, 5.800e7),
(1.680, 5.890e7),
(1.690, 5.980e7),
(1.700, 6.070e7)
]
def unzip(iterable):
'''
function unzips 2-item tuples to lists of each separate item
based on
http://stackoverflow.com/questions/19339/a-transpose-unzip-function-in-python-inverse-of-zip
and
http://stackoverflow.com/questions/13635032/what-is-the-inverse-function-of-zip-in-python
'''
return zip(*iterable)
def abs_in_range(abs):
'''
The function checks to make sure value of absorbance provided is in range
of OD660 values covered by standard curve. Numpy linear interpolation I'll
use doesn't cover handling if value to be checked on curve is not in between
at least two points on curve. Defaults to just giving highest or lowest
corresponding value as shown at http://docs.scipy.org/doc/numpy/reference/generated/numpy.interp.html
when value of 0 given.
Returns true if in range or False when out of range.
Based on
http://stackoverflow.com/questions/618093/how-to-find-whether-a-number-belongs-to-a-particular-range-in-python.
Note that for the unzip, in Python 2 you can get it to seemingly work, or at least not report
an error, if only include one variable to unzip the two items into. However, Python 3 throws
an error for the next line because unpacking gets done wrong and/or cannot compare to a
integers to tuples, which is what it is if not unpacked right. And so best to unpack
both, putting the unused one into the `values_not_used_in_this_function`.
'''
od660_values_list, values_not_used_in_this_function = unzip(
yeast_cell_density_by_OD660_tuples)
return True if min(od660_values_list) <= abs <= max(
od660_values_list) else False
def obtain_value_from_std_curve(abs660):
'''
Function takes absorbance at 660 nm and determines cells per ml using
standard curve of OD660 vs cell density.
Returns cells per ml for the sample.
Approach based on http://stackoverflow.com/questions/25057943/getting-y-value-of-a-curve-given-an-x-value
and
http://docs.scipy.org/doc/numpy/reference/generated/numpy.interp.html
'''
# cast the standard curve tuples list for use as x and y values of
# curve in numpy interpolation
od660_values_list, cells_per_ml_values_list = unzip(
yeast_cell_density_by_OD660_tuples)
# use one-dimensional linear interpolation by Numpy
# http://docs.scipy.org/doc/numpy/reference/generated/numpy.interp.html
return numpy.interp(abs660, od660_values_list, cells_per_ml_values_list)
def represents_float(v):
'''
function to see if an varibale can be typecast a float. Returns True if it
can.
based on
http://stackoverflow.com/questions/1265665/python-check-if-a-string-represents-an-int-without-using-try-except
'''
try:
float(v)
return True
except ValueError:
return False
def cells_per_ml_calc (abs, dilution_factor):
'''
The function takes an absorbance at 660 nm and dilution factor and then
returns an approximation of the number of yeast cells per ml sample and a
boolean indicating if errors involving the provided dilution_factor are
detected. The approach described in the Methods in Yeast Gentics Appendix
entitled "Measuring Yeast Cell Density by Spectrophotometry".
It is an approximation because it is based on a standard curve genrated for
yeast strain A364A and may not be valid for all yeast. If you need accurate
numbers, you should count your cells, or make a standard curve for your own
future use, with a Coulter counter or a hemocytometer.
Dilution factor is in the terms of what number do you need to multiply the
original culture volume to get the final volume,
see http://www.hemocytometer.org/dilution-factor/ . In other words,
dilution_factor equals the fold dilution.
Some examples:
* For a 10-fold dilution, on other words 1 parts original culure plus 9 parts
diluent, the dilution_factor argument is 10.
* For a 5-fold dilution, on other words 1 parts original culure plus 4 parts
diluent, the dilution_factor is 5.
* For a 2-fold or `1-to-1` dilution, on other words 1 parts original culure
plus 1 parts diluent, the dilution_factor is 2.
Technically, no dilution means this factor will be 1 for that case.
However, as calculating that is a bit much to ask of anyone not
actually diluting a sample, to make things easy the dilution factor
argument can be a string 'None', without quotes, and the calculations will
be handled appropriately. Likewise, dilution_factor can also simply be
designated the number `0` treat the sample as undiluted.
The function also returns a Boolean as to whether the dilution_factor was
needed in a calculation, but was not provided in form that can be converted
to a float for multiplication.
'''
# Use provided absorbance to determine cells per ml using standard curve of
# OD660 vs cell density.
calculated_cells_per_ml = obtain_value_from_std_curve(abs)
#**** ACCOUNTING FOR DILUTION FACTOR SECTION*****
# Was planning to typecast dilution_factor to a string to more easily handle comparisons
# next. This conversion allows dilution_factor string case to be lowered for
# comparison; otherwise, when the `.lower()` method is applied in a series
# of comparisons, it throws an error if it is not a string at the time.
# However, oddly in the comparison, it did not throw an error when I tested in IPython. So I didn't onvert first.
dilution_factor_needed_but_not_float_error = False # State will be examined
# if dilution_factor calculation needs to be done later. Helps in feedback
# for when typecast dilution_factor back to float for calculation step.
# NOW APPPLY dilution_factor TO CALCULATION, handling the exception cases of
# dilution_factor of `1`, `none`, or `0` as indicating no dilution of the
# sample.
if (dilution_factor != "1") or (dilution_factor.lower() != "none") or (
dilution_factor.lower() != "zero") or (dilution_factor != "1"):
if represents_float(dilution_factor):
calculated_cells_per_ml = calculated_cells_per_ml * float(dilution_factor)
else:
dilution_factor_needed_but_not_float_error = True
#**** END ACCOUNTING FOR DILUTION FACTOR SECTION *****
return calculated_cells_per_ml, dilution_factor_needed_but_not_float_error
def transformation_inoculation_volume_calc (cells_per_ml):
'''
The function takes the cells per ml of a yeast culture and calculates what
volume to use to inoculate a total of 50 mL of media for carrying out a
typical yeast cell transformation per the approach described in the Methods
in Yeast Gentics Appendix entitled "High-efficiency Transformation of Yeast".
'''
#### *** Use provided cells per ml to determine volume. *** ###
# Final need is 50 ml at 5.0E+06 cells per mL or 2.50E+08 total cells in
# 50 mL.
volume = 2.50E+08/cells_per_ml
return volume
# Make sure provided absorbance in range covered by standard curve
out_of_range_error = abs_in_range(absorbance)
# TO DO: ADD HANDLING OF ERROR
The standard curve resulting from the data points is represented in a visual way in the Out
cell below.
It is interactive.
import plotly.graph_objects as go
import pandas as pd
# code for this originally documented at https://gist.github.com/fomightez/7f445311a78484c83c3210cbd5540192
# but now added in as Plotly version with non-server mode now allows LaTeX in the
# axis labels and now works without credentials being needed in the set-up steps
od660_values_list, cells_per_ml_values_list = unzip(yeast_cell_density_by_OD660_tuples)
cells_per_ml_values_list = [(x/1.0e7) for x in cells_per_ml_values_list] # trying to get them in scale, see http://stackoverflow.com/questions/32542957/control-tick-labels-in-python-seaborn-package
data_dict = {'od':od660_values_list, 'cells_per_ml':cells_per_ml_values_list} # see http://www.gregreda.com/2013/10/26/intro-to-pandas-data-structures/
data_df = pd.DataFrame(data_dict,columns=['od','cells_per_ml']) # see http://www.gregreda.com/2013/10/26/intro-to-pandas-data-structures/
data = [
go.Scatter(
x=data_df['cells_per_ml'], # assign x as the dataframe column 'x'
y=data_df['od']
)
]
layout = go.Layout(
title='Optical Density vs. Cell Density',
xaxis=dict(
title='$\\text{cells per ml (}\\times 10^7{)}$' #latex help from https://plot.ly/python/LaTeX/
),
yaxis=dict(
title='$\\text{OD}_{660}$'
)
)
# IPython notebook
fig=go.Figure(data=data,layout=layout)
fig.show()
The standard curve above is interactive; hover over the curve to easily read values for both variables any where along the curve.
The plot above uses Plotly was has a rich feature set with nice aesthetics by default. Plus, if you go to view this notebook statically at nbviewer, the Plotly plot above is still displayed.
Below the same data is plotted with Bokeh which has nice feature in that you can add a hover tool to make the report you get when you hove over the line more informative. The drawback is that at the time I was writing this it didn't handle nice formatting of the axis labels, specifically LaTeX. Additionally, the Bokeh plot seen below won't render in the static form of this notebook on nbviewer. (<-- Also, need to check if now any hover tool in Plotly now.)
import bokeh
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.io import output_notebook, show
output_notebook() # tells Bokeh to plot inline in the Jupyter notebook
from bokeh.models import HoverTool
import pandas as pd
od660_values_list, cells_per_ml_values_list = unzip(yeast_cell_density_by_OD660_tuples)
cells_per_ml_values_list = [(x/1.0e7) for x in cells_per_ml_values_list] # trying to get them in scale, see http://stackoverflow.com/questions/32542957/control-tick-labels-in-python-seaborn-package
data_dict = {'od':od660_values_list, 'cells_per_ml':cells_per_ml_values_list} # see http://www.gregreda.com/2013/10/26/intro-to-pandas-data-structures/data_dict = {'od':od660_values_list, 'cells_per_ml':cells_per_ml_values_list} # see http://www.gregreda.com/2013/10/26/intro-to-pandas-data-structures/
data_df = pd.DataFrame(data_dict,columns=['od','cells_per_ml']) # see http://www.gregreda.com/2013/10/26/intro-to-pandas-data-structures/
hover = HoverTool(
tooltips=[
("OD, cells/ml", "$y, $x"),
]
)
# plot = figure(tools=[hover], title="Optical Density vs. Cell Density", x_axis_label="cells per ml ($\\times 10^7$)", y_axis_label="OD$_{660}$")
# Seems still working on latex support in Bokeh, and so for now:
plot = figure(tools=[hover], title="Optical Density vs. Cell Density", x_axis_label="cells per ml (x 10^7)", y_axis_label="OD_660")
#plot.xaxis.axis_label_text_font_size = "40pt" #just an example of how size can be controlled
#plot.scatter(data_df['cells_per_ml'],data_df['od'],fill_color=None, fill_alpha=0.6,line_color=None) # Causes hover issues it seems in crowded areas, if leave both.
plot.line(data_df['cells_per_ml'],data_df['od'], line_width = 3)
show(plot); # for the semi-colon at end, see https://groups.google.com/forum/#!msg/jupyter/SMBUkOWPetA/nIVztypABQAJ and http://stackoverflow.com/questions/14506583/suppress-output-of-object-when-plotting-in-ipython
# Performing the actual calculations
cells_per_ml, factor_error_detected = cells_per_ml_calc (absorbance, dilution_factor)
# TO DO: ADD HANDLING OF ERROR
# adjust if diploid --> " Cell densities for diploids are half those for
# halploids as diploids and some mutants which are abnormally large will
# scatter more light than the wildtype haploids at the same cell density."
if haploid == False:
cells_per_ml = cells_per_ml/2
volume_to_inoculate_for_transformation = transformation_inoculation_volume_calc (cells_per_ml)
cells_per_ml
33579999.99999999
cells_per_ml_formatted = "{:.2E}".format(cells_per_ml)
cells_per_ml_formatted
'3.36E+07'
print (cells_per_ml_formatted)
3.36E+07
(I don't know why print command and just calling variable give different results, i.e., one has the single-quotes and the one involving print
doesn't. I guess it is because one is a string and the other is printed string?)
print ("{:.2E}".format(cells_per_ml))
3.36E+07
# adjust if diploid
if haploid == False:
print ("You indicated your strain is a diploid.")
print ("The sample density is "+ "{:.2E}".format(cells_per_ml) +" yeast cells per ml.")
The sample density is 3.36E+07 yeast cells per ml.
# Formatting output of transformation advice
# Formatting correct decimal place for Python 2.7 based on Daren Thomas's answer
# http://stackoverflow.com/questions/6149006/display-a-float-with-two-decimal-places-in-python
print ("If using this sample for inoculating a 50 mL culture for growing to density 2.0E+07 cells per mL for use in a transformation, use " + "{:.1f}".format(
volume_to_inoculate_for_transformation) + " mL of the culture with " + "{:.1f}".format(
50.0 - volume_to_inoculate_for_transformation ) + " mL YPD media.")
If using this sample for inoculating a 50 mL culture for growing to density 2.0E+07 cells per mL for use in a transformation, use 7.4 mL of the culture with 42.6 mL YPD media.