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 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()
!pwd
/Users/sharonnewman/Documents/general/nucleus-devnote-archive-1/devnotes/2026-newman-cytosol-landscape/experiments/20260506-discoveryplate-aria-r3
import os
# print(os.path)

Load Data

# labcraft_platemap = pd.read_csv("experiment_setup_v2_b/labcraft_results/DP-R3b-20260518_112845/concentration_key.csv")
labcraft_platemap = pd.read_csv("data/concentration_key_corrected_mg2000_24p602_mg250_14p862.csv") #I believe Mg 2000 mM was added second...that means with the larger droplet size. Yikes :'(
labcraft_platemap = labcraft_platemap.rename(columns={'Well ID': 'Well'})
labcraft_platemap = labcraft_platemap.set_index("Well")
labcraft_platemap = labcraft_platemap.fillna(0)

samples = pd.read_csv("data/samples_titration_FINAL.csv", index_col="Well")
labcraft_platemap
Loading...
# Normalize column headers: '[ATP]_mM' -> '[ATP] mM'
labcraft_platemap.columns = [str(c).replace("_", " ") for c in labcraft_platemap.columns]

labcraft_platemap.columns
Index(['actual final volume nL', '[ATP] mM', '[Creatine phosphate] mM', '[DNA] nM', '[GTP] mM', '[Magnesium acetate] mM', '[PolyP] mM', '[PPK] uM', '[tRNA] ug/ul', 'Water --'], dtype='object')

# Add base concentration back to titration volumes
from cdk_naming import (
    normalize_reagent_name,
    split_concentration_column_name,
)

base_mm = pd.read_csv(
    "data/base_master_mix_w_smix.csv", index_col="reagent"
)

# normalized reagent key -> (base_conc, unit) lookup
base_lookup = {
    normalize_reagent_name(idx): (row["base_conc_fold"], row["unit"])
    for idx, row in base_mm.iterrows()
}
vol_base_added = 6420

def _reagent_from_col(col):
    """Pull the reagent token from headers like
    '[Magnesium acetate] (mM)', '[ATP]_mM', '[tRNA] ug/ul'.
    """
    # s = str(col).replace("_", " ")
    parts = split_concentration_column_name(col)
    return parts[0] if parts else s  # parts[0] keeps wrappers; normalize strips them

for col in labcraft_platemap.select_dtypes("number").columns:
    key = normalize_reagent_name(_reagent_from_col(col))
    if key in base_lookup:
        base_conc, unit = base_lookup[key]
        labcraft_platemap[col] += base_conc * vol_base_added / labcraft_platemap['actual final volume nL']
        print(f"Added base_conc for '{col}': +{base_conc} {unit}")
    else:
        print(f"  (no base_conc match for '{col}' → key={key!r})")
  (no base_conc match for 'actual final volume nL' → key='actualfinalvolume')
Added base_conc for '[ATP] mM': +3.12 mM
Added base_conc for '[Creatine phosphate] mM': +31.2 mM
  (no base_conc match for '[DNA] nM' → key='dna')
Added base_conc for '[GTP] mM': +3.12 mM
Added base_conc for '[Magnesium acetate] mM': +12.48 mM
  (no base_conc match for '[PolyP] mM' → key='polyp')
  (no base_conc match for '[PPK] uM' → key='ppk')
Added base_conc for '[tRNA] ug/ul': +3.9 ug/ul
  (no base_conc match for 'Water --' → key='water')
labcraft_platemap
Loading...
FACTOR_COLS = labcraft_platemap.columns[1:-1]
# FACTOR_COLS = ['[ATP] mM', '[Creatine phosphate] mM', '[GTP] mM',
#        '[Magnesium acetate] mM', '[PolyP] mM', '[PPK] uM', '[tRNA] ug/ul']
# FACTOR_COLS = ['[ATP]_mM', '[Creatine phosphate]_mM', '[GTP]_mM',
#        '[Magnesium acetate]_mM', '[PolyP]_mM', '[PPK]_uM', '[tRNA]_ug/ul']
# 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
Updated 8 columns in samples from labcraft_platemap: ['[ATP] mM', '[Creatine phosphate] mM', '[DNA] nM', '[GTP] mM', '[Magnesium acetate] mM', '[PolyP] mM', '[PPK] uM', '[tRNA] ug/ul']
Loading...
samples.Name.unique()
array(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', 'ges_r3_01', 'ges_r3_02', 'ges_r3_03', 'ges_r3_04', 'ges_r3_05', 'ges_r3_06', 'ges_r3_07', 'ges_r3_08', 'ges_r3_09', 'ges_r3_10', 'ges_r3_11', 'ges_r3_12', 'ges_r3_13', 'ges_r3_14', 'ges_r3_15', 'ges_r3_16', 'ges_r3_17', 'ges_r3_18', 'ges_r3_19', 'ges_r3_20', 'ges_r3_21', 'ges_r3_22', 'ges_r3_23', 'ges_r3_24', 'ges_r3_25', 'ges_r3_26', 'ges_r3_27', 'ges_r3_28', 'ges_r3_29', 'labcraft_PPK', 'labcraft_ctrl', 'manual_ctrl', 'Fluorescein', 'manual_noDNA'], dtype=object)
samples.to_csv("data/samples_final.csv")
samples.head()
Loading...
samples.index
Index(['H13', 'H14', 'G18', 'H8', 'E6', 'E13', 'K6', 'F14', 'H11', 'G16', ... 'M11', 'N6', 'N7', 'N8', 'M12', 'M13', 'M14', 'M15', 'M16', 'M17'], dtype='object', name='Well', length=118)

DATA ANALYSIS

# Specify file paths
data_file = "data/20260519-132240-cytation5-pure-timecourse-gfp-DiscoveryPlate_R3b-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
Loading...
# data.to_csv("samples_final_with_data.csv")

Plot Raw Curves

# g = pr.plot_curves(data=data, hue="Name")#[data.Type == "Sample"]) #, hue="conditions")
data.Name.unique()
array(['2', '11', '7', 'ges_r3_13', 'labcraft_PPK', '18', '12', 'ges_r3_19', 'labcraft_ctrl', 'ges_r3_11', 'ges_r3_20', 'ges_r3_28', 'ges_r3_22', 'ges_r3_25', 'ges_r3_07', 'ges_r3_24', 'ges_r3_15', '8', '14', '3', 'ges_r3_14', 'ges_r3_01', 'ges_r3_29', '13', 'ges_r3_02', '10', '9', 'ges_r3_12', 'ges_r3_17', 'ges_r3_21', '16', '4', '1', 'ges_r3_08', 'ges_r3_27', '19', '0', 'ges_r3_10', '6', 'ges_r3_26', 'ges_r3_09', '17', 'ges_r3_06', 'ges_r3_04', 'ges_r3_03', '15', 'ges_r3_16', 'ges_r3_23', 'ges_r3_05', 'ges_r3_18', '5', 'manual_ctrl', 'manual_noDNA', 'Fluorescein'], dtype=object)
data.Generator.unique()
array(['lhs', 'ges', 'control', 'manual'], dtype=object)

Normalize Data

data = pr.normalize_data_to_controls(data, ctrl_name = 'Fluorescein')
Data Normalized to Fluorescein 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.Generator == "lhs"])
<Figure size 568x500 with 1 Axes>
g = pr.plot_curves(data=data[data.Generator == "ges"])
<Figure size 622x500 with 1 Axes>
g = pr.plot_curves(data=data[data.Generator.isin(['manual', 'control'])])
data[data.Generator.isin(['manual', 'control'])][FACTOR_COLS]
Loading...
<Figure size 660x500 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
Index(['Time', 'Well', 'Data', 'Row', 'Column', 'Read', 'Plate', 'Clock Time', 'Generator', 'Type', 'Name', '[tRNA] ug/ul', '[Magnesium acetate] mM', '[Creatine phosphate] mM', '[ATP] mM', '[GTP] mM', '[PPK] uM', '[PolyP] mM', '[HEPES] mM', '[Potassium glutamate] mM', '[CTP] mM', '[UTP] mM', '[TCEP] mM', '[Folinic acid] mM', '[Spermidine] mM', '[aas] mM', '[RNAse Inhibitor] U/ml', '[Ribosome] uM', '[Pmix] mg/ml', '[DNA] nM', '[tRNA] ug/ul conc_to_add', '[tRNA] ug/ul 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', '[ATP] mM conc_to_add', '[ATP] mM vol_to_pipette_ul', '[GTP] mM conc_to_add', '[GTP] mM vol_to_pipette_ul', '[PPK] uM conc_to_add', '[PPK] uM vol_to_pipette_ul', '[PolyP] mM conc_to_add', '[PolyP] mM vol_to_pipette_ul', '[DNA] nM conc_to_add', '[DNA] nM 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', 'ctrl_ints', 'data_normalized'], dtype='object')
# Perform kinetic analysis using sigmoid_drift model
kinetics = pr.kinetic_analysis(
    data=data,
    group_by=['Well', 'Name', 'Type', *FACTOR_COLS],  # Group by experimental condition
)
# kinetics = kinetics.reset_index()
kinetics.head()
Loading...
kinetics
Loading...

Visualize Fits on Individual Wells

# Plot kinetic fits 
# g = pr.plot_kinetics(data,  group_by=['Well', 'Name', *FACTOR_COLS])
# g = pr.plot_kinetics(data[data.Type=="Control"],  group_by=['Well', 'Name', *FACTOR_COLS])
# Which samples have 0?

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', 'Name', 'Type', '[ATP]_mM', '[Creatine_phosphate]_mM', '[DNA]_nM', '[GTP]_mM', '[Magnesium_acetate]_mM', '[PolyP]_mM', '[PPK]_uM', '[tRNA]_ug/ul', '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:  115
  Good fits:    85 (73.9%)
  Failed fits:  30 (26.1%)

  R² < 0:       0
  0 ≤ R² < 0.9: 27

  Response metrics (good fits only):
    Steady_State_data_normalized    min=0.0026  median=0.1285  max=0.5510
    Velocity_Max                    min=0.0004  median=0.1772  max=0.8047

  Factor ranges for FAILED wells:
    [ATP]_mM                        [1.992 – 6.192]
    [Creatine_phosphate]_mM         [20.000 – 78.425]
    [DNA]_nM                        [0.000 – 2.270]
    [GTP]_mM                        [1.992 – 5.011]
    [Magnesium_acetate]_mM          [7.967 – 115.116]
    [PolyP]_mM                      [0.000 – 33.700]
    [PPK]_uM                        [0.000 – 4.450]
    [tRNA]_ug/ul                    [2.651 – 5.080]
==================================================
kin.head()
Loading...
import seaborn as sns
kin['steady_state (binned)'] = pd.cut(                                                                                                                                                                                   
  kin['Steady_State_data_normalized'],                                                                                                                                                                        
  bins=[0, 0.05, .2, 0.4, 1.0],
  labels=['0-0.05', '0.05-0.2', '0.2-0.4', '>0.4']
)                                                                                                                                                                                                               
# val = "Steady_State_data_normalized"
val = 'steady_state (binned)'
g = sns.pairplot(
  kin[factors + [val]],
  vars=factors,
  hue=val,
  palette ="rocket_r",
  # palette={'0-0.05': '#d9534f', '0.05-0.2': 'orange', '>0.2': 'blue'},
  plot_kws={'alpha': 0.7, 's': 40},
  diag_kind='hist',
)
<Figure size 2163.11x2000 with 72 Axes>