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_platemapLoading...
# Normalize column headers: '[ATP]_mM' -> '[ATP] mM'
labcraft_platemap.columns = [str(c).replace("_", " ") for c in labcraft_platemap.columns]
labcraft_platemap.columnsIndex(['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_platemapLoading...
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}")
samplesUpdated 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
dataLoading...
# 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"])
g = pr.plot_curves(data=data[data.Generator == "ges"])
g = pr.plot_curves(data=data[data.Generator.isin(['manual', 'control'])])
data[data.Generator.isin(['manual', 'control'])][FACTOR_COLS]Loading...

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.columnsIndex(['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...
kineticsLoading...
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.columnsIndex(['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',
)


