Microbial population dynamics modeling and analysis. Bacterial growth curve fitting (logistic, Gompertz, Baranyi), Lotka-Volterra community dynamics, Gillespie stochastic simulation, biofilm quantification, CFU enumeration, and genome annotation. For metabolic modeling use cobrapy; for sequence analysis use biopython.
61
73%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./backend/cli/skills/biology/microbial-dynamics/SKILL.mdMicrobial Dynamics provides computational tools for modeling and analyzing microbial populations. This skill covers bacterial growth curve fitting using standard models (logistic, Gompertz, Baranyi), multi-species community dynamics via Lotka-Volterra equations, stochastic population simulation using the Gillespie algorithm, biofilm quantification from crystal violet assays, colony-forming unit enumeration with statistical analysis, bacterial genome annotation via Prokka, and simplified anaerobic digestion modeling.
Related Skills: For constraint-based metabolic modeling use cobrapy. For sequence manipulation and BLAST use biopython. For statistical analysis use statistical-analysis.
uv pip install scipy numpy pandas matplotlibFor genome annotation (optional):
# conda install -c bioconda prokkafrom scipy.optimize import curve_fit
import numpy as np
# Logistic growth model
def logistic(t, y0, K, r, lag):
return K / (1 + ((K - y0) / y0) * np.exp(-r * (t - lag)))
# Example OD600 data
time = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24])
od600 = np.array([0.02, 0.02, 0.03, 0.06, 0.15, 0.38, 0.72, 1.05, 1.25, 1.42, 1.48, 1.50, 1.51, 1.51, 1.52])
popt, pcov = curve_fit(logistic, time, od600, p0=[0.02, 1.5, 0.5, 2.0], maxfev=10000)
print(f"y0={popt[0]:.4f}, K={popt[1]:.3f}, r={popt[2]:.3f} h^-1, lag={popt[3]:.2f} h")Fit OD600 data to standard microbial growth models.
import numpy as np
from scipy.optimize import curve_fit
def logistic(t, y0, K, r, lag):
"""Logistic growth model."""
return K / (1 + ((K - y0) / y0) * np.exp(-r * (t - lag)))
def gompertz(t, y0, K, mu_max, lag):
"""Modified Gompertz growth model."""
return y0 + (K - y0) * np.exp(-np.exp((mu_max * np.e / (K - y0)) * (lag - t) + 1))
def baranyi(t, y0, K, mu_max, lag):
"""Baranyi growth model."""
A_t = t + (1 / mu_max) * np.log(np.exp(-mu_max * t) +
np.exp(-mu_max * lag) - np.exp(-mu_max * (t + lag)))
return K - np.log(1 + (np.exp(K) - np.exp(y0)) / np.exp(y0) * np.exp(-mu_max * A_t))
def fit_growth_curve(time, od600, model='logistic'):
"""Fit growth curve to OD data and return parameters."""
models = {'logistic': logistic, 'gompertz': gompertz, 'baranyi': baranyi}
func = models[model]
# Initial guesses
y0_guess = od600[0]
K_guess = od600.max()
r_guess = 0.5
lag_guess = time[np.argmax(np.gradient(od600))] - 1
p0 = [y0_guess, K_guess, r_guess, max(lag_guess, 0)]
bounds = ([0, 0, 0, 0], [np.inf, np.inf, 10, time.max()])
popt, pcov = curve_fit(func, time, od600, p0=p0, bounds=bounds, maxfev=10000)
perr = np.sqrt(np.diag(pcov))
# R-squared
residuals = od600 - func(time, *popt)
ss_res = np.sum(residuals**2)
ss_tot = np.sum((od600 - np.mean(od600))**2)
r_squared = 1 - (ss_res / ss_tot)
param_names = ['y0', 'K', 'mu_max', 'lag']
result = {name: {'value': val, 'std': err}
for name, val, err in zip(param_names, popt, perr)}
result['r_squared'] = r_squared
result['model'] = model
return result, popt
# Compare models
for model_name in ['logistic', 'gompertz', 'baranyi']:
try:
result, _ = fit_growth_curve(time, od600, model=model_name)
print(f"{model_name}: R²={result['r_squared']:.4f}, "
f"mu_max={result['mu_max']['value']:.3f} ± {result['mu_max']['std']:.3f} h⁻¹, "
f"lag={result['lag']['value']:.2f} h")
except RuntimeError:
print(f"{model_name}: fitting failed")Simulate multi-species interactions.
import numpy as np
from scipy.integrate import solve_ivp
def lotka_volterra(t, N, r, K, alpha):
"""Generalized Lotka-Volterra for n species.
Args:
N: array of population sizes
r: array of intrinsic growth rates
K: array of carrying capacities
alpha: interaction matrix (alpha[i,j] = effect of j on i)
"""
n = len(N)
dNdt = np.zeros(n)
for i in range(n):
interaction = sum(alpha[i, j] * N[j] for j in range(n))
dNdt[i] = r[i] * N[i] * (1 - interaction / K[i])
return dNdt
# 3-species community
r = np.array([0.5, 0.4, 0.3]) # Growth rates
K = np.array([1000, 800, 600]) # Carrying capacities
alpha = np.array([ # Interaction matrix
[1.0, 0.5, 0.1], # Species 1: self + competition from 2 and 3
[0.3, 1.0, 0.4], # Species 2
[0.2, 0.6, 1.0], # Species 3
])
N0 = np.array([10, 10, 10]) # Initial populations
sol = solve_ivp(
lotka_volterra, [0, 100], N0,
args=(r, K, alpha),
t_eval=np.linspace(0, 100, 500),
method='RK45'
)
print("Final populations:")
for i in range(3):
print(f" Species {i+1}: {sol.y[i, -1]:.1f}")
# Stability analysis: check if coexistence is stable
A = np.diag(1/K) @ alpha
try:
eigenvalues = np.linalg.eigvals(A)
stable = all(ev.real > 0 for ev in eigenvalues)
print(f"Coexistence equilibrium is {'stable' if stable else 'unstable'}")
except np.linalg.LinAlgError:
print("Stability analysis failed")Gillespie SSA for exact stochastic simulation.
import numpy as np
def gillespie_ssa(propensity_func, stoich_matrix, x0, t_end, max_steps=100000):
"""Gillespie Stochastic Simulation Algorithm.
Args:
propensity_func: function(x) -> array of reaction propensities
stoich_matrix: reactions x species stoichiometry matrix
x0: initial state vector
t_end: simulation end time
"""
t = 0
x = np.array(x0, dtype=float)
times = [t]
states = [x.copy()]
for step in range(max_steps):
props = propensity_func(x)
total_prop = np.sum(props)
if total_prop == 0 or t >= t_end:
break
# Time to next reaction
dt = np.random.exponential(1 / total_prop)
t += dt
if t > t_end:
break
# Choose reaction
reaction = np.searchsorted(np.cumsum(props), np.random.uniform(0, total_prop))
reaction = min(reaction, len(props) - 1)
# Update state
x += stoich_matrix[reaction]
x = np.clip(x, 0, None)
times.append(t)
states.append(x.copy())
return np.array(times), np.array(states)
# Birth-death-immigration model
def propensities(x):
N = x[0]
birth_rate = 0.5 * N
death_rate = 0.01 * N * (N - 1) # Density-dependent death
immigration = 5.0
return np.array([birth_rate, death_rate, immigration])
stoich = np.array([[1], [-1], [1]]) # Birth: +1, Death: -1, Immigration: +1
# Run ensemble
n_runs = 50
results = []
for i in range(n_runs):
times, states = gillespie_ssa(propensities, stoich, [10], t_end=50)
results.append((times, states))
# Summary statistics
final_pops = [states[-1, 0] for _, states in results]
print(f"Mean final population: {np.mean(final_pops):.1f} ± {np.std(final_pops):.1f}")Process crystal violet biofilm assay data.
import numpy as np
import pandas as pd
def analyze_biofilm_cv(od_data, blank_od=0.05, conditions=None):
"""Analyze crystal violet biofilm assay.
Args:
od_data: dict of condition -> list of OD570 replicates
blank_od: blank well OD for background subtraction
"""
results = []
for condition, replicates in od_data.items():
corrected = np.array(replicates) - blank_od
corrected = np.clip(corrected, 0, None)
results.append({
'condition': condition,
'mean_od': np.mean(corrected),
'std_od': np.std(corrected, ddof=1),
'n': len(corrected),
'sem': np.std(corrected, ddof=1) / np.sqrt(len(corrected))
})
df = pd.DataFrame(results)
# Normalize to control
control_mean = df.iloc[0]['mean_od']
df['fold_change'] = df['mean_od'] / control_mean
return df
# Example: biofilm inhibition dose-response
od_data = {
'Control': [0.85, 0.92, 0.88, 0.90],
'1 uM': [0.80, 0.78, 0.82, 0.79],
'10 uM': [0.55, 0.52, 0.58, 0.50],
'100 uM': [0.20, 0.18, 0.22, 0.19],
}
df = analyze_biofilm_cv(od_data)
print(df[['condition', 'mean_od', 'std_od', 'fold_change']])Calculate colony-forming units from serial dilution plating.
import numpy as np
from scipy import stats
def calculate_cfu(counts, dilution_factor, volume_plated_ml=0.1):
"""Calculate CFU/mL from plate counts.
Args:
counts: list of colony counts per plate
dilution_factor: dilution used (e.g., 1e-6 for 10^-6)
volume_plated_ml: volume plated in mL
"""
counts = np.array(counts)
cfu_per_ml = counts / (dilution_factor * volume_plated_ml)
mean_cfu = np.mean(cfu_per_ml)
std_cfu = np.std(cfu_per_ml, ddof=1)
sem = std_cfu / np.sqrt(len(counts))
# 95% confidence interval
ci = stats.t.interval(0.95, df=len(counts)-1, loc=mean_cfu, scale=sem)
return {
'mean_cfu_per_ml': mean_cfu,
'std': std_cfu,
'sem': sem,
'ci_95': ci,
'n': len(counts),
'log10_cfu': np.log10(mean_cfu)
}
# Example
result = calculate_cfu(counts=[42, 38, 45], dilution_factor=1e-6, volume_plated_ml=0.1)
print(f"CFU/mL: {result['mean_cfu_per_ml']:.2e}")
print(f"Log10 CFU/mL: {result['log10_cfu']:.2f}")
print(f"95% CI: ({result['ci_95'][0]:.2e}, {result['ci_95'][1]:.2e})")Run Prokka and parse results.
import subprocess
import pandas as pd
def run_prokka(fasta_path, output_dir, prefix='genome', genus=None, species=None):
"""Annotate bacterial genome with Prokka."""
cmd = [
'prokka', fasta_path,
'--outdir', output_dir,
'--prefix', prefix,
'--cpus', '4',
'--force'
]
if genus:
cmd.extend(['--genus', genus])
if species:
cmd.extend(['--species', species])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"Prokka failed: {result.stderr}")
return f'{output_dir}/{prefix}'
def parse_prokka_gff(gff_path):
"""Parse Prokka GFF3 output to extract gene statistics."""
genes = []
with open(gff_path) as f:
for line in f:
if line.startswith('#') or line.startswith('>'):
continue
if '\t' not in line:
continue
parts = line.strip().split('\t')
if len(parts) < 9:
continue
feature_type = parts[2]
if feature_type in ('CDS', 'tRNA', 'rRNA', 'tmRNA'):
attrs = dict(kv.split('=', 1) for kv in parts[8].split(';') if '=' in kv)
genes.append({
'type': feature_type,
'start': int(parts[3]),
'end': int(parts[4]),
'strand': parts[6],
'gene': attrs.get('gene', ''),
'product': attrs.get('product', ''),
'length': int(parts[4]) - int(parts[3]) + 1
})
df = pd.DataFrame(genes)
print(f"Total features: {len(df)}")
print(f"Feature types:\n{df['type'].value_counts()}")
print(f"Mean CDS length: {df[df['type']=='CDS']['length'].mean():.0f} bp")
# Coding density
total_coding = df[df['type'] == 'CDS']['length'].sum()
genome_size = df['end'].max()
coding_density = 100 * total_coding / genome_size
print(f"Coding density: {coding_density:.1f}%")
return dfSimplified ADM1 for biogas prediction.
import numpy as np
from scipy.integrate import solve_ivp
def adm1_simplified(t, y, params):
"""Simplified anaerobic digestion model.
State: [S_substrate, X_acidogens, X_methanogens, S_VFA, CH4]
"""
S, Xa, Xm, VFA, CH4 = y
k_hyd = params['k_hyd'] # Hydrolysis rate
mu_a = params['mu_a'] # Acidogen max growth rate
Ks_a = params['Ks_a'] # Acidogen half-saturation
mu_m = params['mu_m'] # Methanogen max growth rate
Ks_m = params['Ks_m'] # Methanogen half-saturation
Y_a = params['Y_a'] # Acidogen yield
Y_m = params['Y_m'] # Methanogen yield
kd = params['kd'] # Decay rate
Ki = params['Ki'] # VFA inhibition constant
# Hydrolysis
r_hyd = k_hyd * S
# Acidogenesis (Monod kinetics)
r_acid = mu_a * (S / (Ks_a + S)) * Xa
# Methanogenesis (Monod with VFA inhibition)
inhibition = Ki / (Ki + VFA)
r_meth = mu_m * (VFA / (Ks_m + VFA)) * Xm * inhibition
dSdt = -r_hyd - r_acid / Y_a
dXa = Y_a * r_acid - kd * Xa
dXm = Y_m * r_meth - kd * Xm
dVFA = r_acid - r_meth / Y_m
dCH4 = r_meth
return [dSdt, dXa, dXm, dVFA, dCH4]
params = {
'k_hyd': 0.25, 'mu_a': 0.5, 'Ks_a': 200,
'mu_m': 0.2, 'Ks_m': 50, 'Y_a': 0.1,
'Y_m': 0.05, 'kd': 0.02, 'Ki': 3000
}
y0 = [5000, 100, 50, 100, 0] # Initial conditions
sol = solve_ivp(adm1_simplified, [0, 60], y0, args=(params,),
t_eval=np.linspace(0, 60, 300), method='RK45')
print(f"Final substrate: {sol.y[0, -1]:.0f} mg/L")
print(f"Total CH4 produced: {sol.y[4, -1]:.0f} mg/L")import pandas as pd
import numpy as np
# Load multi-condition OD600 data
data = pd.read_csv('growth_data.csv') # columns: time, condition, od600
results = []
for condition, group in data.groupby('condition'):
time = group['time'].values
od = group['od600'].values
fit, popt = fit_growth_curve(time, od, model='gompertz')
fit['condition'] = condition
results.append(fit)
print(f"{condition}: mu_max={fit['mu_max']['value']:.3f} h⁻¹, "
f"lag={fit['lag']['value']:.1f} h, K={fit['K']['value']:.3f}")import numpy as np
from scipy.integrate import solve_ivp
r = np.array([0.5, 0.4, 0.3])
K = np.array([1000, 800, 600])
alpha = np.array([[1.0, 0.5, 0.1], [0.3, 1.0, 0.4], [0.2, 0.6, 1.0]])
N0 = [10, 10, 10]
sol = solve_ivp(lotka_volterra, [0, 200], N0, args=(r, K, alpha),
t_eval=np.linspace(0, 200, 1000), method='RK45')
for i in range(3):
print(f"Species {i+1}: equilibrium = {sol.y[i, -1]:.0f}")prefix = run_prokka('assembly.fasta', 'prokka_output', genus='Escherichia', species='coli')
genes_df = parse_prokka_gff(f'{prefix}.gff')
print(f"\nCDS count: {len(genes_df[genes_df['type'] == 'CDS'])}")
print(f"tRNA count: {len(genes_df[genes_df['type'] == 'tRNA'])}")
print(f"rRNA count: {len(genes_df[genes_df['type'] == 'rRNA'])}")RK45 for non-stiff systems, BDF or Radau for stiff systems (common in multi-species models)Problem: Growth curve fit fails to converge
Solution: Adjust initial parameter guesses closer to expected values. Increase maxfev. Check that data has sufficient points during lag and exponential phases.
Problem: Lotka-Volterra simulation diverges
Solution: Reduce step size or use adaptive solver. Check that interaction matrix doesn't produce negative populations — use events parameter in solve_ivp to stop at zero.
Problem: Gillespie SSA runs too slowly Solution: For large populations (>10000), switch to tau-leaping approximation. Or use ODE mean-field approximation and add noise analytically.
Problem: Prokka fails with "no genes found"
Solution: Check FASTA file is properly formatted (no extra whitespace). Verify sequences are bacterial. Use --kingdom Bacteria flag explicitly.
dc00e07
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.