Made in Invert | Golden Batch
Golden Batch Comparison: PD-GB vs Campaign Runs
A 1 L golden-batch reference (PD-GB) compared against a 13-run iPSC scale-up campaign from 1 L to 10 L — tracking differentiation-marker trajectories and each run's deviation from the golden profile.
Golden Batch Comparison: PD-GB vs Campaign Runs
Context
The golden batch (PD-GB) was run at 1L scale with media lot ML-2025-A. This campaign includes 13 production runs: 2 at 1L (PD-047, PD-048) and 11 at 10L (PD-049 through PD-059), representing a scale-up campaign. All runs used the same iPSC bank (iPSC-WCB-003). The differentiation process follows three phases: Floor Plate Induction (days 0–5) → Midbrain Patterning (days 5–11) → Neuronal Maturation (days 11–21).
Differentiation Marker Trajectories vs Golden Batch
Code · 31 lines
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
markers_list = ['TH+ (%)', 'FOXA2+ (%)', 'LMX1A+ (%)', 'NURR1+ (%)']
worst_runs = ['PD-053-DIFF', 'PD-051-DIFF', 'PD-055-DIFF', 'PD-058-DIFF', 'PD-049-DIFF']
fig, axes = plt.subplots(2, 2, figsize=(14, 9))
for idx, marker in enumerate(markers_list):
ax = axes[idx // 2, idx % 2]
for run in diff_markers_timeseries['Run Name'].unique():
run_data = diff_markers_timeseries[diff_markers_timeseries['Run Name'] == run].dropna(subset=[marker])
if run == 'PD-GB-DIFF':
ax.plot(run_data['Elapsed Time (hrs)'] / 24, run_data[marker],
'k-', linewidth=2.5, marker='o', markersize=6, label='PD-GB (Golden)', zorder=10)
elif run in worst_runs:
ax.plot(run_data['Elapsed Time (hrs)'] / 24, run_data[marker],
'--', linewidth=1.5, marker='s', markersize=4, alpha=0.8, label=run.replace('-DIFF',''))
else:
ax.plot(run_data['Elapsed Time (hrs)'] / 24, run_data[marker],
'-', linewidth=0.8, alpha=0.3, color='gray')
ax.set_title(marker, fontsize=12, fontweight='bold')
ax.set_xlabel('Time (days)')
ax.set_ylabel(marker)
ax.legend(fontsize=7, loc='upper left')
ax.grid(True, alpha=0.3)
plt.suptitle('Differentiation Markers vs Golden Batch (PD-GB)', fontsize=13, fontweight='bold')
plt.tight_layout()
plt.show()Process Parameter Deviations (pH, DO, Temperature)
Code · 32 lines
import matplotlib.pyplot as plt
worst_runs = ['PD-053-DIFF', 'PD-051-DIFF', 'PD-055-DIFF', 'PD-058-DIFF', 'PD-049-DIFF']
fig, axes = plt.subplots(3, 1, figsize=(14, 11))
params = [
('[Parent] Online pH Process Value (pH)', 'pH'),
('[Parent] Dissolved Oxygen (DO) (%)', 'DO (%)'),
('[Parent] Temperature (°C)', 'Temperature (°C)')
]
for idx, (col, label) in enumerate(params):
ax = axes[idx]
for run in diff_process_timeseries['Run Name'].unique():
run_data = diff_process_timeseries[diff_process_timeseries['Run Name'] == run].dropna(subset=[col])
if run == 'PD-GB-DIFF':
ax.plot(run_data['Elapsed Time (hrs)'] / 24, run_data[col],
'k-', linewidth=2, label='PD-GB (Golden)', zorder=10)
elif run in worst_runs:
ax.plot(run_data['Elapsed Time (hrs)'] / 24, run_data[col],
'--', linewidth=1.2, alpha=0.8, label=run.replace('-DIFF',''))
else:
ax.plot(run_data['Elapsed Time (hrs)'] / 24, run_data[col],
'-', linewidth=0.5, alpha=0.25, color='gray')
ax.set_ylabel(label, fontsize=11)
ax.set_xlabel('Time (days)')
ax.legend(fontsize=8, loc='best', ncol=2)
ax.grid(True, alpha=0.3)
ax.set_title(f'{label} — All Differentiation Runs vs Golden Batch', fontsize=11)
plt.tight_layout()
plt.show()Metabolite Profiles
Code · 36 lines
import matplotlib.pyplot as plt
worst_runs = ['PD-053-DIFF', 'PD-051-DIFF', 'PD-055-DIFF', 'PD-058-DIFF', 'PD-049-DIFF']
fig, axes = plt.subplots(2, 2, figsize=(14, 9))
metab_cols = [
('[Parent] Glucose Concentration (g/L)', 'Glucose (g/L)'),
('Lactate (g/L)', 'Lactate (g/L)'),
('Ammonia (mM)', 'Ammonia (mM)'),
('Glutamine (mM)', 'Glutamine (mM)')
]
for idx, (col, label) in enumerate(metab_cols):
ax = axes[idx // 2, idx % 2]
for run in diff_metabolites_timeseries['Run Name'].unique():
run_data = diff_metabolites_timeseries[diff_metabolites_timeseries['Run Name'] == run].dropna(subset=[col])
if len(run_data) == 0:
continue
if run == 'PD-GB-DIFF':
ax.plot(run_data['Elapsed Time (hrs)'] / 24, run_data[col],
'k-', linewidth=2, label='PD-GB (Golden)', zorder=10)
elif run in worst_runs:
ax.plot(run_data['Elapsed Time (hrs)'] / 24, run_data[col],
'--', linewidth=1.2, alpha=0.8, label=run.replace('-DIFF',''))
else:
ax.plot(run_data['Elapsed Time (hrs)'] / 24, run_data[col],
'-', linewidth=0.5, alpha=0.25, color='gray')
ax.set_title(label, fontsize=11, fontweight='bold')
ax.set_xlabel('Time (days)')
ax.set_ylabel(label)
ax.legend(fontsize=7, loc='best')
ax.grid(True, alpha=0.3)
plt.suptitle('Metabolite Profiles vs Golden Batch', fontsize=13, fontweight='bold')
plt.tight_layout()
plt.show()Deviation Heatmap
Code · 33 lines
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import seaborn as sns
# Build deviation table
diff_runs = assist_data_properties[assist_data_properties['Unit Operation Type'] == 'Directed Differentiation'].copy()
gb_diff = diff_runs[diff_runs['Run Name'] == 'PD-GB-DIFF'].iloc[0]
metrics = {
'TH+ (%) (last)': 'TH+ Δ (%)',
'FOXA2+ (%) (last)': 'FOXA2+ Δ (%)',
'NURR1+ (%) (last)': 'NURR1+ Δ (%)',
'[Parent] Viability (%) (last)': 'Viability Δ (%)'
}
rows = []
for _, row in diff_runs[diff_runs['Run Name'] != 'PD-GB-DIFF'].iterrows():
entry = {'Run': row['Run Name'].replace('-DIFF', '')}
for col, label in metrics.items():
entry[label] = round(row[col] - gb_diff[col], 1) if pd.notna(row[col]) and pd.notna(gb_diff[col]) else None
rows.append(entry)
dev_df = pd.DataFrame(rows).sort_values('TH+ Δ (%)').set_index('Run')
fig, ax = plt.subplots(figsize=(12, 7))
sns.heatmap(dev_df, annot=True, fmt='.1f', cmap='RdYlGn', center=0,
linewidths=0.5, ax=ax, vmin=-25, vmax=10,
cbar_kws={'label': 'Deviation from Golden Batch (%)'})
ax.set_title('Differentiation Outcome Deviations from Golden Batch (PD-GB)', fontsize=13, fontweight='bold')
ax.set_ylabel('')
plt.tight_layout()
plt.show()Comprehensive Deviation Summary
Code · 58 lines
import pandas as pd
import numpy as np
diff_runs = assist_data_properties[assist_data_properties['Unit Operation Type'] == 'Directed Differentiation'].copy()
gb_diff = diff_runs[diff_runs['Run Name'] == 'PD-GB-DIFF'].iloc[0]
summary_data = []
for _, row in diff_runs[diff_runs['Run Name'] != 'PD-GB-DIFF'].iterrows():
run = row['Run Name']
th_delta = row['TH+ (%) (last)'] - gb_diff['TH+ (%) (last)']
foxa2_delta = row['FOXA2+ (%) (last)'] - gb_diff['FOXA2+ (%) (last)']
nurr1_delta = row['NURR1+ (%) (last)'] - gb_diff['NURR1+ (%) (last)']
viab_delta = row['[Parent] Viability (%) (last)'] - gb_diff['[Parent] Viability (%) (last)']
# Process stats
run_proc = diff_process_timeseries[diff_process_timeseries['Run Name'] == run]
do_min = run_proc['[Parent] Dissolved Oxygen (DO) (%)'].min() if len(run_proc) > 0 else None
temp_max = run_proc['[Parent] Temperature (°C)'].max() if len(run_proc) > 0 else None
# Root cause
root_cause = ''
if run == 'PD-051-DIFF':
root_cause = 'Temperature excursion (39.7°C, 48h) — incubator malfunction'
elif run == 'PD-055-DIFF':
root_cause = 'DO limitation (14%, 88h) — agitation not scaled from 1L'
elif run == 'PD-053-DIFF':
root_cause = 'Glucose depletion + lactate accumulation (metabolic shift)'
elif run == 'PD-058-DIFF':
root_cause = 'Glutamine depletion + ammonia toxicity (3.4 mM)'
elif run == 'PD-049-DIFF':
root_cause = 'Media lot change (ML-2025-B) — FOXA2+ impacted'
elif run == 'PD-056-DIFF':
root_cause = 'Transient DO dip (22.4%, 33.5h) — minor'
elif run == 'PD-059-DIFF':
root_cause = 'Transient DO dip (22.7%, 33.8h) — minor'
# Severity
if abs(th_delta) > 10 or abs(nurr1_delta) > 15 or viab_delta < -5:
severity = 'HIGH'
elif abs(th_delta) > 5 or abs(foxa2_delta) > 5 or abs(nurr1_delta) > 5:
severity = 'MODERATE'
else:
severity = 'LOW'
summary_data.append({
'Run': run.replace('-DIFF', ''),
'TH+ Δ (%)': round(th_delta, 1),
'FOXA2+ Δ (%)': round(foxa2_delta, 1),
'NURR1+ Δ (%)': round(nurr1_delta, 1),
'Viability Δ (%)': round(viab_delta, 1),
'DO min (%)': round(do_min, 1) if do_min is not None else None,
'Temp max (°C)': round(temp_max, 1) if temp_max is not None else None,
'Severity': severity,
'Root Cause': root_cause
})
summary_df = pd.DataFrame(summary_data).sort_values('TH+ Δ (%)')
summary_dfIdentified Deviations — Root Cause Analysis
PD-053 — Glucose Depletion and Metabolic Shift (TH+ Δ = -16.9%)
Glucose depleted to <0.5 g/L between days 9–21 (vs ~2.6 g/L at day 9 for the golden batch). Lactate accumulated to 5.5 g/L (vs ~1 g/L for GB), indicating a glycolytic metabolic shift. The feed schedule was identical to other runs — cells consumed glucose at a higher rate, possibly linked to lower seed density at expansion (VCD 2.36 vs 2.81 for GB). No operator observation was logged explaining the metabolic shift.
PD-051 — Temperature Excursion (TH+ Δ = -10.9%, Viability Δ = -8.5%)
Documented incubator malfunction: temperature reached 39.7°C for ~48 hours (days 14–16) during the Neuronal Maturation phase. This is the most sensitive phase for TH+ expression. Viability damage carried through to formulation (85.5% vs 93.7% for GB).
PD-055 — Dissolved Oxygen Limitation (TH+ Δ = -9.8%)
DO dropped to 14% for 88 hours (days 9.3–13.0). Documented root cause: agitation setpoint was 80 rpm — not scaled from the 1L protocol (which used 120 rpm at bench scale). Resulted in lower final VCD (3.10 vs 3.44 for GB) and lower formulation cell concentration (9.2 vs 10.32 ×10⁶ cells/mL).
PD-058 — Glutamine Depletion and Ammonia Toxicity (NURR1+ Δ = -17.8%)
Severe glutamine depletion (final 0.11 mM vs 1.28 mM for GB) with ammonia accumulation to 3.42 mM (vs 1.36 mM for GB — 2.5× higher). NURR1+ expression was the most impacted marker, consistent with ammonia toxicity affecting late-stage neuronal maturation.
PD-049 — Media Lot Effect (FOXA2+ Δ = -10.9%)
First run on media lot ML-2025-B (transition from ML-2025-A). Documented investigation note: FOXA2+ significantly lower, attributed to media lot change. FOXA2+ is an early floor plate marker — suggests the new media lot affected early patterning efficiency.
PD-056 and PD-059 — Transient DO Dips (minor)
Both experienced transient DO drops to ~22% for ~33 hours (days 7.6–9.0). Impact was minimal: TH+ deviations of only -2.6% and -0.9% respectively.
Campaign Statistics and Scale-Up Context
Golden Batch (PD-GB): 1L vessel, Media Lot ML-2025-A. Campaign mean TH+ (excluding GB): 77.9% ± 5.2% (range 66.3–84.9%). Runs within ±5% of GB TH+: 6 of 13. Runs with HIGH severity deviations: 3 of 13 (PD-053, PD-051, PD-058).
The 10L runs (ML-2025-C) that did not experience process excursions (PD-050, PD-052, PD-054, PD-056, PD-059) achieved mean TH+ of 81.9%, suggesting the scale-up itself introduces only a modest ~1.3 percentage point reduction relative to the golden batch.
Data Sources and Assumptions
Golden batch reference: PD-GB-DIFF endpoint measurements (TH+ 83.2%, FOXA2+ 85.6%, LMX1A+ 77.9%, NURR1+ 74.5%, VCD 3.44 ×10⁶ cells/mL, Viability 95.2%)
Severity classification: HIGH = |TH+ Δ| > 10% OR |NURR1+ Δ| > 15% OR Viability Δ < -5%; MODERATE = |TH+ Δ| > 5% OR |FOXA2+ Δ| > 5% OR |NURR1+ Δ| > 5%; LOW = all others
DO excursion threshold: <30% (below which oxygen limitation affects cell metabolism)
Temperature excursion threshold: >38°C (above normal 37°C setpoint)
All deviations calculated as absolute difference from golden batch endpoint values
iPSC bank lot (iPSC-WCB-003) was consistent across all runs
More reports
Richelle et al. (2022) Digital Twin — Model-Based CHO Intensification
A from-scratch implementation of the mechanistic CHO growth model from Richelle et al. (2022) as an executable digital twin — five kinetic parameters identified from fed-batch data, predicting culture dynamics from fed-batch through intensified perfusion.
View reportReal-Time MVDA Batch Monitoring — Raw Material Lot Variability Detection
Multivariate statistical process control detecting an out-of-spec media lot in a 2000 L CHO fed-batch run — a 25-batch Normal Operating Condition reference flags raw-material lot-to-lot variability that passed CoA release.
View reportPCA Reference Model — SiteB Tech Transfer Comparability
A PCA reference model trained on 84 primary-site runs, then used to project 15 SiteB tech-transfer runs and assess comparability — reference-only training that mirrors validating a model before new runs arrive.
View report