Setup

!pip install nucleus-cdk==0.5.0rc2 | tail -n2

# Surface a failed install here, rather than as a confusing ModuleNotFoundError
# in the import cell below.
import importlib.metadata as md
assert md.version("nucleus-cdk") == "0.5.0rc2", f"got {md.version('nucleus-cdk')}"
Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages (from requests->panel>=1.0->hvplot<0.12.0,>=0.11.1->nucleus-cdk==0.5.0rc2) (2.5.0)
Requirement already satisfied: certifi>=2017.4.17 in /opt/homebrew/Caskroom/miniforge/base/lib/python3.12/site-packages (from requests->panel>=1.0->hvplot<0.12.0,>=0.11.1->nucleus-cdk==0.5.0rc2) (2025.11.12)

Analyze time-series fluorescence data from PURE (Protein synthesis Using Recombinant Elements) experiments.

Setup

%load_ext autoreload
%autoreload 2
    
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# Import the cdk platereader module
from cdk.analysis.cytosol import platereader as pr

# Set up plotting
pr.plot_setup()

Load Data

labcraft_platemap = pd.read_csv("data/concentration_key.csv")
labcraft_platemap = labcraft_platemap.fillna(0)
labcraft_platemap = labcraft_platemap.rename(columns={'Well ID': 'well_id'})
labcraft_platemap = labcraft_platemap.set_index("well_id")
labcraft_platemap = labcraft_platemap.drop(columns=['[PMix]_mg/mL', '[DNA]_nM']) #did not use labcraft here

samples = pd.read_csv("data/samples_titration.csv", index_col="well_id")
samples["Type"] = "Sample"
samples.columns
Index(['Name', '[DNA] (nM)', '[RNAse Inhib] (U/mL)', '[PMix] (mg/mL)', '[Ribosome] (uM)', 'Rxn Volume (uL)', '[Magnesium acetate] (mM)', '[Creatine phosphate] (mM)', '[Potassium glutamate] (mM)', 'hepes mM', 'ppk uM', 'polyphosphate mM', 'atp mM', 'gtp mM', 'ctp mM', 'utp mM', 'tcep mM', 'folinic_acid mM', 'spermidine mM', 'aas mM', 'trna ug/ul', '[DNA] (nM) conc_to_add', '[DNA] (nM) vol_to_pipette_ul', '[PMix] (mg/mL) conc_to_add', '[PMix] (mg/mL) vol_to_pipette_ul', '[Magnesium acetate] (mM) conc_to_add', '[Magnesium acetate] (mM) vol_to_pipette_ul', '[Creatine phosphate] (mM) conc_to_add', '[Creatine phosphate] (mM) vol_to_pipette_ul', '[Potassium glutamate] (mM) conc_to_add', '[Potassium glutamate] (mM) vol_to_pipette_ul', 'base_master_mix_vol_to_add_ul', 'total_titration_vol_to_add_ul', 'buffer_vol_to_add_ul', 'total_vol_check_ul', 'Type'], dtype='object')
labcraft_platemap.columns
Index(['[Creatine phosphate]_mM', '[Magnesium acetate]_mM', '[Potassium glutamate]_mM', 'Water_--'], dtype='object')
# samples = samples.reset_index()
samples.head()
Loading...
# Map concentration_key column names to samples column names
col_rename = {
    '[Creatine phosphate]_mM': '[Creatine phosphate] (mM)',
    '[Magnesium acetate]_mM': '[Magnesium acetate] (mM)',
    '[Potassium glutamate]_mM': '[Potassium glutamate] (mM)',
}
labcraft_platemap = labcraft_platemap.rename(columns=col_rename)


# --- Add base master mix contributions to get true final concentrations ---
base_mm = pd.read_csv("data/base_master_mix.csv", index_col="reagent")
base_mg   = base_mm.loc['magnesium_acetate', 'base_conc']    # 2.469 mM
base_kglu = base_mm.loc['potassium_glutamate', 'base_conc']  # 60.0 mM
# Creatine phosphate not in base_mm → base contribution is 0

labcraft_platemap['[Magnesium acetate] (mM)']   += base_mg
labcraft_platemap['[Potassium glutamate] (mM)'] += base_kglu
!pwd
/private/tmp/claude-501/-Users-sharonnewman-Documents-general-nucleus-skills/4a8d6065-9e0a-4e14-b326-0bffb6d3273b/scratchpad/clean/20260306-discoveryplate-aria-r0


# Overwrite common concentration columns in samples with labcraft_platemap values
# Use .update() so only matching well_ids are overwritten (no NaNs for missing wells)
common_cols = labcraft_platemap.columns.intersection(samples.columns).tolist()
samples.update(labcraft_platemap[common_cols])
print(f"Updated {len(common_cols)} columns in samples from labcraft_platemap: {common_cols}")
samples.head()
Updated 3 columns in samples from labcraft_platemap: ['[Creatine phosphate] (mM)', '[Magnesium acetate] (mM)', '[Potassium glutamate] (mM)']
Loading...
# Add standard wells not in DOE samples
standards = pd.DataFrame([
    {"well_id": "K8", "Type": "Standard", "Name": "Standard"},
    {"well_id": "K9", "Type": "Standard", "Name": "Standard"},
    {"well_id": "K10", "Type": "Standard", "Name": "Standard"},
]).set_index("well_id")
samples = pd.concat([samples, standards])
samples["[Magnesium acetate] (mM)"] = samples["[Magnesium acetate] (mM)"].apply(lambda x: x*95.2/203.3) # Perhaps through artifact miststake, we used the wrong calculation, so inpute Mg might have been wrong
samples["Well"] = samples.index

samples.to_csv("data/samples_final.csv")
samples.Well.unique()
array(['J16', 'H9', 'J10', 'G16', 'I14', 'G11', 'G12', 'G10', 'I15', 'I12', 'I10', 'I11', 'G8', 'J9', 'I8', 'H8', 'J13', 'G15', 'G14', 'H11', 'H15', 'J11', 'G13', 'G9', 'J12', 'H16', 'H12', 'I13', 'H14', 'H13', 'I9', 'J14', 'J15', 'I16', 'H10', 'J8', 'K8', 'K9', 'K10'], dtype=object)
# Specify file paths
data_file = "data/20260309-123116-cytation5-pure-timecourse-gfp-biotek-cdk.txt"
platemap_file = "data/samples_final.csv"


# Load data
data, platemap = pr.load_platereader_data(
    data_file=data_file,
    platemap_file=platemap_file,
    platereader="biotek-cdk"  # Options: "cytation", "envision", "biotek-cdk"
)

# Checkout first few rows
data.head()
# data["conditions"] = data.groupby(factors).ngroup()
# data["Name"] = data["conditions"]

# data = data[~data["Column"].isin([19])]
Loading...
data.Well.unique()
array(['G8', 'G9', 'G10', 'G11', 'G12', 'G13', 'G14', 'G15', 'G16', 'H8', 'H9', 'H10', 'H11', 'H12', 'H13', 'H14', 'H15', 'H16', 'I8', 'I9', 'I10', 'I11', 'I12', 'I13', 'I14', 'I15', 'I16', 'J8', 'J9', 'J10', 'J11', 'J12', 'J13', 'J14', 'J15', 'J16', 'K8', 'K9', 'K10'], dtype=object)

Plot Raw Curves

g = pr.plot_curves(data=data, hue="Well")#[data.Type == "Sample"]) #, hue="conditions")
<Figure size 579x500 with 1 Axes>
data.Name.unique()
array(['5', '8', '3', '2', '7', '6', '1', '12', '9', '10', '11', '4', 'Standard'], dtype=object)

Normalize Data

data = pr.normalize_data_to_controls(data, ctrl_name = 'Standard')
Data Normalized to Standard in col data_normalized. The active column for subsequent operations is: data_normalized

Now replot your curves to see them normalized

g = pr.plot_curves(data=data[data.Type == "Sample"])
<Figure size 568x500 with 1 Axes>

Kinetic Analysis

Metrics extracted:

  • Vmax (Velocity Max): Maximum rate of fluorescence increase (slope at inflection point)

  • Lag time: Time to reach the exponential phase

  • Steady-state: Final fluorescence level and time to reach 95% of asymptote

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

  • R²: Goodness of fit

data.columns
FACTOR_COLS = ['[PMix] (mg/mL)','[DNA] (nM)','[Magnesium acetate] (mM)','[Potassium glutamate] (mM)', '[Creatine phosphate] (mM)']
# Perform kinetic analysis using sigmoid_drift model
kinetics = pr.kinetic_analysis(
    data=data,
    group_by=['Well',  *FACTOR_COLS],  # Group by experimental condition
)
# kinetics = kinetics.reset_index()
kinetics.head()
Loading...
data.head()
Loading...

Visualize Fits on Individual Wells

# Plot kinetic fits 
g = pr.plot_kinetics(data[data.Well == "I10"],  group_by=['Well', 'well_id', *FACTOR_COLS])
<Figure size 1800x400 with 1 Axes>

Summary Plots

# pr.plot_summary(data)
responses = ["steady_state", "vmax", "lag_time_min"]

kin = kinetics.reset_index()
kin.columns = [
    "_".join([str(x).replace(" ", "_") for x in col if x])
    for col in kin.columns
]
factors = [str(x).replace(" ", "_") for x in FACTOR_COLS]
kin.head()
Loading...
kin.columns
Index(['Well', '[PMix]_(mg/mL)', '[DNA]_(nM)', '[Magnesium_acetate]_(mM)', '[Potassium_glutamate]_(mM)', '[Creatine_phosphate]_(mM)', 'Velocity_Time', 'Velocity_data_normalized', 'Velocity_Max', 'Lag_Time', 'Lag_data_normalized', 'Steady_State_Time', 'Steady_State_data_normalized', 'Fit_params', 'Fit_R^2', 'Fit_drift', 'Fit_good_fit'], dtype='object')
from plate_qc import (
        qc_summary, plot_replicate_agreement,
        plot_plate_heatmap, plot_qc_overview,
    )
responses = ['Steady_State_data_normalized', 'Velocity_Max']
clean, failed = qc_summary(kin, factors, responses)
==================================================
DOE QC Summary
==================================================
  Total wells:  36
  Good fits:    29 (80.6%)
  Failed fits:  7 (19.4%)

  R² < 0:       1
  0 ≤ R² < 0.9: 5

  Response metrics (good fits only):
    Steady_State_data_normalized    min=0.0059  median=0.0334  max=0.6116
    Velocity_Max                    min=0.0061  median=0.0361  max=0.4999

  Factor ranges for FAILED wells:
    [PMix]_(mg/mL)                  [1.530 – 2.031]
    [DNA]_(nM)                      [1.000 – 20.000]
    [Magnesium_acetate]_(mM)        [1.156 – 8.180]
    [Potassium_glutamate]_(mM)      [72.500 – 200.000]
    [Creatine_phosphate]_(mM)       [0.000 – 100.000]
==================================================
kin.head()
Loading...
import seaborn as sns
kin['steady_state (binned)'] = pd.cut(                                                                                                                                                                                   
  kin['Steady_State_data_normalized'],                                                                                                                                                                        
  bins=[0, 0.05, 0.2, 1.0],                                                                                                                                                                                   
  labels=['0-0.05', '0.05-0.2', '>0.2']                                                                                                                                                                               
)                                                                                                                                                                                                               

g = sns.pairplot(
  kin[factors + ['steady_state (binned)']],
  vars=factors,
  hue='steady_state (binned)',
  palette={'0-0.05': '#d9534f', '0.05-0.2': 'orange', '>0.2': 'green'},
  plot_kws={'alpha': 0.7, 's': 40},
  diag_kind='hist',
)
<Figure size 1413.11x1250 with 30 Axes>