EsaR Repressor Titration

Analyze time-series data from platereader experiments.

EsaR Repressor Titration

try:
    import cdk
    print("CDK already installed, skipping installation and restart.")
except ImportError:
    !pip install nucleus-cdk==0.6.2 -q > /dev/null 2>&1
    print("\nInstallation complete. Restarting runtime to refresh dependencies...")
    import os
    os._exit(0)

import pandas as pd
pd.set_option('display.max_columns', None)
import seaborn as sns
import matplotlib.pyplot as plt

# Import the cdk platereader module
from cdk.instruments import platereader as pr

from cdk import logging
log = logging.setup_logging(logging.ERROR)

Load Data

%%writefile drive_utils.py
import os
import re

import gdown


def get_drive_file_id(url):
    """Extract the file ID from a Google Drive share URL."""
    match = re.search(r'/d/([a-zA-Z0-9_-]+)', url)
    if match:
        return match.group(1)
    return None


def download_from_drive(files_info, destination_dir):
    """
    Download files from Google Drive.

    Args:
        files_info (list): List of dicts with 'url' and 'filename'.
        destination_dir (str): Local path to save files.
    Returns:
        dict: Mapping of filenames to local paths.
    """
    os.makedirs(destination_dir, exist_ok=True)

    paths = {}
    for item in files_info:
        url = item['url']
        filename = item['filename']
        dest = os.path.join(destination_dir, filename)
        file_id = get_drive_file_id(url)

        if file_id:
            gdown.download(id=file_id, output=dest, quiet=True)
        else:
            gdown.download(url, output=dest, quiet=True)

        paths[filename] = dest
    return paths
Writing drive_utils.py
# Set use_drive = True to download data_file and platemap_file from Google Drive instead of
# using local files. Each Drive file must have its permissions set to "Anyone with a link" can "view".
use_drive = True

if use_drive:
    from drive_utils import download_from_drive

    files_to_download = [
        {
            "url": "https://drive.google.com/file/d/1hIHRX1OryR1oOKmk6P2DNQ848UiDwyZb/view?usp=sharing",  # Replace with your data file link.
            "filename": "raw_data.txt",
        },
        {
            "url": "https://drive.google.com/file/d/13M87Y409x-2NTbcbAjeqRYIXsV6QUuFb/view?usp=sharing",  # Replace with your platemap file link.
            "filename": "platemap.csv",
        },
    ]
    downloaded_paths = download_from_drive(files_to_download, destination_dir="data")
    data_file = downloaded_paths["raw_data.txt"]
    platemap_file = downloaded_paths["platemap.csv"]
else:
    data_file = "sample-data/20251111-122213-cytation5-pure-timecourse-gfp-MFG-98-tRNA-QC.txt"
    platemap_file = "sample-data/platemap.csv"

print(f"Data file: {data_file}\nPlatemap file: {platemap_file}")
Data file: data/raw_data.txt
Platemap file: data/platemap.csv
# Load data
result = pr.load_platereader_data(
    data_file=data_file,
    platemap_file=platemap_file,
    platereader="biotek" # options: "biotek"
)

The output is a list of PlateReaderResult objects; if you did more than one read on the plate reader, the results will be in separate objects.

print(result)
PlateReaderResult with the following 2 blocks:
0: (4307, 41) kinetic read with reads: GFP-G70:485,528 (Fluorescence) (Plate 'Plate 1')
1: (4307, 41) kinetic read with reads: GFP-Gext:485,528 (Fluorescence) (Plate 'Plate 1')

Here there were two reads with different gains but the same excitation/emission spectrum, on one plate. If we want just the GFP-Gext read, this corresponds to block index 1, so we can extract that block by indexing:

desired_index = 1
data = result[desired_index]

You can see the underlying data with data.view() (which returns a Pandas DataFrame):

data.view().head()
Loading...

Plot Raw Curves

data.plot(style='Type', hue='Name')
<seaborn.axisgrid.FacetGrid at 0x7c60197374d0>
<Figure size 785.625x500 with 1 Axes>
data_EsaO = data.select(name=["EsaO-mNG + 0uL EsaR", "EsaO-mNG + 0uL EsaR + AHSL","EsaO-mNG + 7uL EsaR", "EsaO-mNG + 7uL EsaR + AHSL"])
data_EsaO_2 = data.select(name=["[EsaO]2-mNG + 0uL EsaR", "[EsaO]2-mNG + 0uL EsaR + AHSL","[EsaO]2-mNG + 7uL EsaR", "[EsaO]2-mNG + 7uL EsaR + AHSL"])

df_1 = data_EsaO.data
df_2 = data_EsaO_2.data


# convert timedelta -> float hours
for df in (df_1, df_2):
    df['Time_hrs'] = df['Time'].dt.total_seconds() / 3600

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10,5), sharex=True, sharey=True)

sns.lineplot(data=df_1, x='Time_hrs', y='Data', hue='Name', ax=ax1)
sns.lineplot(data=df_2, x='Time_hrs', y='Data', hue='Name', ax=ax2)

ax1.set(title='Single operator (EsaO)', xlabel="", ylabel="")
ax2.set(title='Dual operator ([EsaO]2)', xlabel="", ylabel="")
fig.supylabel("RFU", y=0.55)
fig.supxlabel("Time (hrs)", x=0.55)


for ax in (ax1, ax2):
    ax.legend(frameon=False)

sns.despine(fig)
fig.tight_layout()
<Figure size 1000x500 with 2 Axes>

We can see that the standard (here, HPTS) is stable and can be safely used for normalization.

Normalize Data

Now, use data.normalize('<standard name>') to normalize your data. This will calculate the average over a time window at the end of the experiment (by default, 1 hour), then divide all data by the mean of that window-average across all wells with the same Name (defined by your platemap).

# To check the exact name of your standard
platemap = data.platemap
standards = platemap[platemap['Type']=='Standard']
standards['Name'].unique()
array(['Fluorescein 0uM', 'Fluorescein 0.5uM', 'Fluorescein 1uM', 'Fluorescein 2uM'], dtype=object)
data = data.normalize('10 uM HPTS')

Now replot your curves to see them normalized:

g = data.plot(style='Type', exclude_types=['Standard'])
# g.savefig('after-normalization.png',dpi=300)
<Figure size 735.75x500 with 1 Axes>

If you want to change the time window over which you calculate the average, you can use the window argument:

# data = data.normalize('10 uM HPTS', window=pd.Timedelta("2.5h"))

Kinetic Analysis

See the DevNote on kinetic analysis for more details.

Metrics extracted:

  • Maximum velocity: Maximum rate of fluorescence increase (slope at inflection point)

  • Lag time: Time to reach the exponential phase

  • Steady-state: Final fluorescence level

  • Time-to-completion: Time it takes to reach 95% of asymptote

  • Drift: Rate of signal decay or increase after steady-state

  • R²: Goodness of fit; “Good Fit” is True if R2≥0.95R^2 \geq 0.95

# Perform kinetic analysis using sigmoid_drift model
kinetics = data.fit_kinetics()
[07/16/26 17:29:20] INFO     Calculating Kinetics                analysis.py:223
kinetics.summary
Loading...

Visualize Fits

g = kinetics.plot(col="Well")
# g = kinetics.plot() # call this to visualize across replicates
<Figure size 1800x2400 with 18 Axes>

Summary Plots

g = kinetics.plot_summary()
<Figure size 6000x1500 with 4 Axes>

Key Metrics Explained

1. Steady-State Level (Steady State, Data)

  • The final fluorescence value reached by the reaction

  • Represents the total amount of protein produced

  • Higher values indicate greater expression yield

2. Maximum Velocity (Velocity, Max)

  • The steepest slope of the fluorescence curve (at the inflection point)

  • Units: RFU per second

  • Reflects the peak rate of protein synthesis

  • Sensitive to enzyme activity, substrate availability, and reaction conditions

3. Lag Time (Lag, Time)

  • Time before exponential fluorescence increase begins

  • May reflect time for ribosome assembly or initial translation steps

  • Shorter lag times suggest faster reaction initiation

4. Drift (Fit, drift)

  • Rate of fluorescence change after reaching steady-state

  • Positive drift: continued synthesis or aggregation

  • Negative drift: photobleaching, protein degradation, or quenching

  • Units: RFU per second

5. R² Value (Fit, R^2)

  • Goodness of fit (0 to 1, higher is better)

  • R² > 0.98 indicates excellent fit

  • Poor fits may indicate noisy data, overflow errors, or non-sigmoid kinetics


Tips and Troubleshooting

  • Overflow errors: Wells with OVRFLW or NaN values are automatically excluded from fitting

  • Poor fits (low R²): Inspect raw curves for anomalies (bubbles, evaporation, pipetting errors)

  • Drift: Sometimes seen in kinetics curves; use sigmoid_drift model

  • Multiple replicates: Always include technical replicates and report error bars

  • Comparing conditions: Normalize or blank data consistently across all samples


Next Steps

  • Export kinetics results: pr.export_kinetics(kinetics, 'results.csv')

  • Statistical analysis: Use scipy.stats or statsmodels for ANOVA/t-tests

  • Parameter optimization: Vary Mg²⁺, K⁺, or other conditions to maximize Vmax or steady-state

  • Mechanistic modeling: Fit ODE models to extract biological rate constants