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


