eachanjohnson's picture
Sync from scbirlab/tutorial-autorep-analysis via hub-sync
881ad94 verified
Raw History Blame Contribute Delete
18.5 kB
from functools import partial
import os
from carabiner.mpl import add_legend, grid, colorblind_palette
import gradio as gr
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
from scipy.stats import multivariate_hypergeom, nbinom
# Set the default color cycle
mpl.rcParams['axes.prop_cycle'] = mpl.cycler(
color=("lightgrey", "dimgrey") + colorblind_palette(name="vibrant_0")[1:],
)
SEED: int = 42
MAX_TIME: float = 5.
SOURCES_DIR: str = "sources"
E_MAX: float = 2. # transcript rate per cell, with no dCas9 bound
GROWTH_RATE: float = 1. # intrinsic growth of non-targeting strains
PSEUDOCOUNT: float = 1. # only to avoid log(0)
N_NT: int = 2 # non-targeting guides, pooled as the reference
STRENGTH_DEFAULTS = [1., 5., 20.]
FITNESS_DEFAULTS = [.5, 2., .2]
N_GUIDES: int = len(STRENGTH_DEFAULTS)
STRAIN_LABELS = (
[f"Non-targeting {i + 1}" for i in range(N_NT)]
+ [f"Guide {i + 1}" for i in range(N_GUIDES)]
)
def inject_markdown(filename):
with open(os.path.join(SOURCES_DIR, filename), 'r') as f:
md = f.read()
return gr.Markdown(
md,
latex_delimiters=[
{"left": "$$", "right": "$$", "display": True},
{"left": "$", "right": "$", "display": False},
],
)
def colors():
return mpl.rcParams['axes.prop_cycle'].by_key()['color']
def dynamics(t, y, w, K, KR, d, k_R):
"""Cells and per-cell transcripts in a pool.
y : cells per strain, then transcripts per cell per strain
w : intrinsic growth rate per strain
K : carrying capacity of the pool
KR : K_i * R at full dCas9 induction, per strain
d : mRNA decay rate
k_R : rate at which dCas9 accumulates after induction
"""
n_strains = len(w)
n, m = y[:n_strains], y[n_strains:]
r = w * (1. - np.sum(n) / K) # instantaneous per-capita growth
occupancy = KR * (1. - np.exp(-k_R * t)) # K_i R(t)
dn = r * n
dm = E_MAX / (1. + occupancy) - (d + r) * m
return np.concatenate([dn, dm])
def calculate_pool(
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate,
*guide_params,
n_timepoints=100,
):
strength, fitness = guide_params[:N_GUIDES], guide_params[N_GUIDES:]
w = GROWTH_RATE * np.asarray([1.] * N_NT + list(fitness))
KR = np.asarray([0.] * N_NT + list(strength))
K = inoculum * carrying_capacity
inoculum_var = inoculum + inoculum_var * np.square(inoculum)
p = inoculum / inoculum_var
n = (inoculum ** 2.) / (inoculum_var - inoculum)
n0 = nbinom.rvs(n, p, size=len(w), random_state=SEED).astype(float)
# before induction, dCas9 is off and growth is unaffected by the guides
m0 = E_MAX / (mrna_decay + GROWTH_RATE * (1. - np.sum(n0) / K))
t = np.linspace(0., MAX_TIME, num=int(n_timepoints))
ode_solution = solve_ivp(
dynamics,
t_span=(0., MAX_TIME),
t_eval=t,
y0=np.concatenate([n0, np.full(len(w), m0)]),
args=(w, K, KR, mrna_decay, dcas9_rate),
rtol=1e-8,
atol=1e-8,
)
cells = ode_solution.y[:len(w)]
transcripts_per_cell = ode_solution.y[len(w):]
r = w[:,None] * (1. - np.sum(cells, axis=0, keepdims=True) / K)
occupancy = KR[:,None] * (1. - np.exp(-dcas9_rate * t[None]))
qss = E_MAX / (1. + occupancy) / (mrna_decay + r)
return t, cells, transcripts_per_cell, qss
def plotter_t(x, growths, scatter=False, dashed=None, vline=None, aspect_ratio=None, **kwargs):
fig, axes = grid(aspect_ratio=aspect_ratio or 1.5)
plotter_f = partial(axes.scatter, s=5.) if scatter else axes.plot
for i, y in enumerate(growths):
plotter_f(
x.flatten(), y.flatten(),
label=STRAIN_LABELS[i],
)
if dashed is not None:
for i, y in enumerate(dashed):
axes.plot(
x.flatten(), y.flatten(),
color=colors()[i],
linestyle="--",
label="_none",
)
axes.plot([], [], color="black", linestyle="--", label="Steady-state prediction")
if vline is not None:
axes.axvline(vline, color="grey", linestyle=":", label="_none")
axes.set(
xlabel="Time",
yscale="log",
**kwargs,
)
add_legend(axes)
return fig
def plotter_ref(
x, growths, scatter=False, fitlines=None, open_points=None, text=None, **kwargs,
):
fig, axes = grid(aspect_ratio=1.5 if text is None else 1.7)
plotter_f = partial(axes.scatter, s=5.) if scatter else axes.plot
for i, y in enumerate(growths):
plotter_f(
x.flatten(), y.flatten(),
label=STRAIN_LABELS[i],
)
if open_points is not None:
# samples excluded from the fit, to keep the domain unchanged
open_x, open_growths = open_points
for i, y in enumerate(open_growths):
axes.scatter(
open_x.flatten(), y.flatten(),
s=5.,
facecolors="none",
edgecolors=colors()[i],
label="_none",
)
axes.scatter(
[], [],
s=5.,
facecolors="none",
edgecolors="black",
label="Not fitted",
)
if fitlines is not None:
fit_x, intercepts, fit_x_all = fitlines
for i, b in enumerate(intercepts.flatten()):
# gradient fixed at 1 on log-log axes: dashed where extrapolated, solid where fitted
axes.plot(
fit_x_all.flatten(), fit_x_all.flatten() * np.exp(b),
color=colors()[i],
linestyle="--",
label="_none",
)
axes.plot(
fit_x.flatten(), fit_x.flatten() * np.exp(b),
color=colors()[i],
label="_none",
)
if text is not None:
axes.text(
1.05, 0.,
text,
fontsize=10,
va="top",
transform=axes.transAxes,
)
axes.set(
xscale="log",
yscale="log",
**kwargs,
)
axes.xaxis.set_major_formatter(mpl.ticker.ScalarFormatter())
axes.xaxis.set_minor_formatter(mpl.ticker.NullFormatter())
add_legend(axes)
return fig
def plotter_estimates(true_strength, estimated_strength, text=None, **kwargs):
fig, axes = grid(aspect_ratio=1.5 if text is None else 1.7)
for i, (true, estimated) in enumerate(zip(true_strength, estimated_strength)):
axes.scatter(
[true], [estimated],
s=40.,
color=colors()[i + N_NT],
label=STRAIN_LABELS[i + N_NT],
)
lims = [min(true_strength) / 2., max(true_strength) * 2.]
axes.plot(lims, lims, color="lightgrey", linestyle="--", zorder=0, label="_none")
if text is not None:
axes.text(
1.05, 0.,
text,
fontsize=10,
va="top",
transform=axes.transAxes,
)
axes.set(
xscale="log",
yscale="log",
**kwargs,
)
add_legend(axes)
return fig
def pool_plotter(
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
):
t, cells, transcripts_per_cell, qss = calculate_pool(
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
)
initial = transcripts_per_cell[:,:1]
return [
plotter_t(
t,
cells,
ylabel="Number of cells per strain",
),
plotter_t(
t,
transcripts_per_cell / initial,
dashed=qss / initial,
ylabel="Transcripts per cell,\nrelative to $t=0$",
aspect_ratio=1.6,
),
]
def reads_sampler(population, sample_frac, seq_depth, reps, variance, capture=None, seed=SEED):
samples = []
for i, timepoint_pop in enumerate(np.split(population.astype(int), population.shape[-1], axis=-1)):
sample_size = np.floor(timepoint_pop.sum() * sample_frac).astype(int)
samples.append(
multivariate_hypergeom.rvs(
m=timepoint_pop.flatten(),
n=sample_size,
size=reps,
random_state=seed + i,
).T
)
samples = np.stack(samples, axis=-2)
if capture is not None:
samples = samples * capture[:,None,None]
read_means = np.floor(seq_depth * samples.shape[0] * samples / samples.sum(axis=0, keepdims=True))
# negative binomial with variance = mean + variance * mean ** 2
n = 1. / variance
p = 1. / (1. + variance * read_means)
return nbinom.rvs(n, p, random_state=seed)
def capture_efficiency(n_strains, spread, seed):
"""Per-guide capture and amplification efficiency, constant across samples."""
return np.exp(np.random.default_rng(seed).normal(scale=spread, size=n_strains))
def calculate_reads(
sample_frac, seq_reps, seq_depth, read_var, capture_spread,
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
):
t, cells, transcripts_per_cell, qss = calculate_pool(
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
n_timepoints=10,
)
n_strains = cells.shape[0]
dna_counts = reads_sampler(
cells,
sample_frac, seq_depth, seq_reps, read_var,
capture=capture_efficiency(n_strains, capture_spread, SEED + 1),
seed=SEED + 1000,
)
rna_counts = reads_sampler(
cells * transcripts_per_cell,
sample_frac, seq_depth, seq_reps, read_var,
capture=capture_efficiency(n_strains, capture_spread, SEED + 2),
seed=SEED + 2000,
)
return t, dna_counts, rna_counts
def normalised_ratio(dna_counts, rna_counts):
"""A_i = (x_i / c_i) / (x_i(0) / c_i(0)), with t=0 counts summed over replicates."""
x0 = rna_counts[:,0].sum(axis=-1)
c0 = dna_counts[:,0].sum(axis=-1)
log_r0 = np.log((x0 + PSEUDOCOUNT) / (c0 + PSEUDOCOUNT))
log_A = (
np.log((rna_counts + PSEUDOCOUNT) / (dna_counts + PSEUDOCOUNT))
- log_r0[:,None,None]
)
return log_A, x0, c0
def autorep_fitter(t, dna_counts, rna_counts, late_time):
log_A, x0, c0 = normalised_ratio(dna_counts, rna_counts)
late_start = max(1, int(np.flatnonzero(t >= late_time)[0]) if np.any(t >= late_time) else len(t) - 1)
# pooled non-targeting reference for each sample
nt_ratio = rna_counts[:N_NT].sum(axis=0) / dna_counts[:N_NT].sum(axis=0)
log_A0 = np.log(nt_ratio / (x0[:N_NT].sum() / c0[:N_NT].sum()))
y = log_A - log_A0[None]
b = y[:,late_start:].mean(axis=(1, 2))
return log_A, log_A0, b, late_start
def reads_plotter(
sample_frac, seq_reps, seq_depth, read_var, capture_spread,
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
):
t, dna_counts, rna_counts = calculate_reads(
sample_frac, seq_reps, seq_depth, read_var, capture_spread,
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
)
log_A, x0, c0 = normalised_ratio(dna_counts, rna_counts)
t_tiled = np.tile(t[:,None], (1, seq_reps))
return pool_plotter(
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
) + [
plotter_t(
t_tiled,
dna_counts,
scatter=True,
ylabel="gDNA read counts per strain",
),
plotter_t(
t_tiled,
rna_counts,
scatter=True,
ylabel="RNA read counts per strain",
),
plotter_t(
t_tiled,
np.exp(log_A),
scatter=True,
ylabel="$A_i = \\frac{x_i(t)}{c_i(t)} / \\frac{x_i(0)}{c_i(0)}$",
),
]
def fit_plotter(
late_time,
sample_frac, seq_reps, seq_depth, read_var, capture_spread,
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
):
t, dna_counts, rna_counts = calculate_reads(
sample_frac, seq_reps, seq_depth, read_var, capture_spread,
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
)
log_A, log_A0, b, late_start = autorep_fitter(t, dna_counts, rna_counts, late_time)
true_strength = list(guide_params[:N_GUIDES])
estimated_strength = np.expm1(-b[N_NT:])
plot_text = "\n".join(
f"Guide {i + 1}: $\\hat{{b}}={bi:.2f}$"
for i, bi in enumerate(b[N_NT:])
) + (
"\nNon-targeting: $\\hat{b}=$"
+ ", ".join(f"${bi:.2f}$" for bi in b[:N_NT])
+ "\n(should be 0; spread is the noise floor)"
)
rank_recovered = np.array_equal(
np.argsort(estimated_strength),
np.argsort(true_strength),
)
estimates_text = "\n".join(
f"Guide {i + 1}: true {true:.1f}, estimated {estimated:.1f}"
for i, (true, estimated) in enumerate(zip(true_strength, estimated_strength))
) + f"\nRank order recovered: {'yes' if rank_recovered else 'no'}"
reference = np.exp(log_A0)
late_reference = reference[late_start:]
fit_x = np.geomspace(late_reference.min(), late_reference.max(), num=20)
fit_x_all = np.geomspace(reference.min(), reference.max(), num=20)
return reads_plotter(
sample_frac, seq_reps, seq_depth, read_var, capture_spread,
inoculum, inoculum_var, carrying_capacity, mrna_decay, dcas9_rate, *guide_params,
) + [
plotter_ref(
late_reference,
np.exp(log_A[:,late_start:]),
scatter=True,
fitlines=(fit_x, b, fit_x_all),
open_points=(reference[:late_start], np.exp(log_A[:,:late_start])),
text=plot_text,
xticks=np.round(np.geomspace(reference.min(), reference.max(), num=3), 2),
xlabel="$A_0$, pooled non-targeting",
ylabel="$A_i$",
),
plotter_estimates(
true_strength,
estimated_strength,
text=estimates_text,
xlabel="True $K_iR$",
ylabel="Estimated $K_iR$",
),
]
with gr.Blocks() as demo:
inject_markdown("header.md")
# Autorep and transcript dynamics
inject_markdown("intro.md")
inject_markdown("transcript-dynamics.md")
with gr.Row():
guide_strength = [
gr.Slider(
.1, 100.,
step=.1,
value=k,
label=f"Strength $K R$, guide {i + 1}",
)
for i, k in enumerate(STRENGTH_DEFAULTS)
]
with gr.Row():
guide_fitness = [
gr.Slider(
0., 2.,
step=.05,
value=w,
label=f"Relative growth rate, guide {i + 1}",
)
for i, w in enumerate(FITNESS_DEFAULTS)
]
with gr.Row():
mrna_decay = gr.Slider(
.05, 100.,
step=.01,
value=2.,
label="mRNA decay rate",
)
dcas9_rate = gr.Slider(
.005, 1.,
step=.005,
value=.8,
label="dCas9 accumulation rate",
)
with gr.Row():
inoculum = gr.Slider(
10, 1_000_000,
step=10,
value=1000,
label="Average inoculum per strain",
)
inoculum_var = gr.Slider(
.001, 1.,
step=.001,
value=.001,
label="Inoculum variance between strains",
)
carrying_capacity = gr.Slider(
N_NT + N_GUIDES + 1, 10_000,
step=1,
value=10,
label="Total carrying capacity (x inoculum)",
)
plot_pool = gr.Button("Plot growth and transcript levels")
pool_curves_n = gr.Plot(label="Growth vs time", format="png")
pool_curves_m = gr.Plot(label="Transcripts per cell vs time", format="png")
pool_curves = [pool_curves_n, pool_curves_m]
pool_inputs = [
inoculum, inoculum_var, carrying_capacity,
mrna_decay, dcas9_rate,
*guide_strength, *guide_fitness,
]
# Read counts
inject_markdown("read-counts.md")
with gr.Row():
sample_frac = gr.Slider(
.001, 1., step=.001,
value=.1,
label="Fraction of population per sample",
)
seq_reps = gr.Slider(
1, 10,
step=1,
value=3,
label="Technical replicates",
)
seq_depth = gr.Slider(
10, 10_000,
step=10,
value=10_000,
label="Average reads per strain per sample",
)
with gr.Row():
read_var = gr.Slider(
.001, 1.,
step=.001,
value=.001,
label="Sequencing variance",
)
capture_spread = gr.Slider(
0., 1.,
step=.05,
value=.5,
label="Per-guide capture variability (log-scale SD)",
)
plot_reads = gr.Button("Plot read counts")
read_curves_dna = gr.Plot(label="gDNA read counts vs time", format="png")
read_curves_rna = gr.Plot(label="RNA read counts vs time", format="png")
read_curves_ratio = gr.Plot(label="Normalised RNA/DNA ratio vs time", format="png")
read_curves = [read_curves_dna, read_curves_rna, read_curves_ratio]
read_inputs = [sample_frac, seq_reps, seq_depth, read_var, capture_spread]
# Fitting
inject_markdown("nuisance-parameter.md")
late_time = gr.Slider(
0., MAX_TIME,
step=.1,
value=0.,
label="Use samples after time",
)
plot_fit = gr.Button("Estimate sgRNA strength")
fit_curves_ref = gr.Plot(label="Normalised ratio vs non-targeting reference", format="png")
fit_curves_estimates = gr.Plot(label="Estimated vs true strength", format="png")
fit_curves = [fit_curves_ref, fit_curves_estimates]
# Events
plot_pool.click(
fn=pool_plotter,
inputs=pool_inputs,
outputs=pool_curves,
)
plot_reads.click(
fn=reads_plotter,
inputs=read_inputs + pool_inputs,
outputs=pool_curves + read_curves,
)
plot_fit.click(
fn=fit_plotter,
inputs=[late_time] + read_inputs + pool_inputs,
outputs=pool_curves + read_curves + fit_curves,
)
if __name__ == "__main__":
demo.queue()
demo.launch(share=True)