systems-biology/model-curation/SKILL.md
Validates, gap-fills, and standardizes genome-scale metabolic models using memote for consistency and annotation scoring and COBRApy for manual curation, including mass/charge balance, energy-generating-cycle detection, dead-end resolution, GPR fixes, and SBML/SBO/MIRIAM annotation. Use when improving a draft model, gap-filling to a target medium, detecting erroneous ATP-from-nothing cycles, interpreting a memote score correctly (consistency vs biological validity), validating predictions against measured growth/essentiality, or preparing a model for publication.
npx skillsauth add GPTomics/bioSkills bio-systems-biology-model-curationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Reference examples tested with: memote 0.17+, COBRApy 0.29+, Python 3.10+
Before using code patterns, verify installed versions match. If versions differ:
pip show <package> then help(module.function) to check signatures<tool> --version then <tool> --help to confirm flagsIf code throws ImportError, AttributeError, or TypeError, introspect the installed package and adapt the example to match the actual API rather than retrying.
Note: a memote SCORE is comparable only within a memote version (the test suite evolves). The Python entry points are memote.suite.api.test_model / snapshot_report (not run/snapshot), and cobra.flux_analysis.gapfill has no demand argument (it gap-fills toward the model's objective).
"Validate and improve the quality of my metabolic model" -> Score the model for consistency and annotation with memote, then use COBRApy to fix mass/charge imbalances, remove energy-generating cycles, gap-fill deliberately, and validate predictions against data.
memote report snapshot model.xml --filename report.htmlcobra.flux_analysis.gapfill(), mass/charge balance and energy-cycle checks (COBRApy)The most common misconception in the field is that a high memote score means a good model. memote's scored total is a weighted sum of stoichiometric consistency, mass/charge balance, annotation coverage (KEGG/ChEBI/BiGG), SBO-term presence, and SBML/FBC conformance. All of that is HYGIENE - it measures whether the model is well-formed and well-annotated, NOT whether it predicts biology. A model can score 90% and mispredict every knockout and every growth phenotype. Optimizing the score for its own sake is Goodhart's law made concrete.
Curation therefore has two separable axes, and both must be reported:
The single most dangerous defect a high score can hide is an energy-generating cycle: a set of reactions that produces ATP/NADH from nothing. It arises from reversible reactions and blind gap-filling, passes mass balance, inflates growth, and invalidates every flux prediction. Test for it explicitly.
| Symptom | Action | Tool |
|---------|--------|------|
| Model cannot grow on the target medium | gap-fill toward the objective, from a universal DB | cobra.flux_analysis.gapfill |
| Growth is implausibly high / ATP from nothing | detect and break energy-generating cycles | max-ATP-with-no-uptake test; constrain directionality |
| Reactions unbalanced | fix mass/charge (usually protons at pH 7) | per-reaction element/charge sum |
| Metabolite never produced or never consumed | resolve dead-end (add reaction or fix stoichiometry) | connectivity scan |
| Low annotation / SBO score | add MIRIAM annotations and SBO terms | memote report + manual/annotation tools |
| Predictions wrong despite high score | validate against measured growth/essentiality | separate experimental comparison (not memote) |
pip install memote
memote run model.xml # run the test suite (pytest-based)
memote report snapshot model.xml --filename report.html # human-readable HTML report
# Programmatic entry points (verify against the installed memote version):
from memote.suite.api import test_model, snapshot_report
code, result = test_model(model, results=True) # result is a MemoteResult (the raw test outcomes)
html = snapshot_report(result, html=True) # render the same report programmatically
# Read WHICH tests fail (consistency, energy cycles, unbalanced reactions) -- the total % is not
# a measure of biological correctness.
Goal: Prove the model cannot manufacture any energy currency (ATP, NADH, NADPH, FADH2, ...) from nothing.
Approach: Close every exchange so no nutrients enter and zero the ATP-maintenance lower bound (its NGAM floor would otherwise make a closed model infeasible for the wrong reason). Then, for EACH energy currency, add a moiety-conserving dissipation reaction (charged -> discharged) and maximize it. A result of 0 (or infeasible) per currency is correct; any positive finite flux is an erroneous energy-generating cycle to trace and fix by constraining reaction directionality. EGCs are not ATP-only, so the sweep must cover every currency present (Fritzemeier 2017); proton-motive-force cycles are subtler and are handled by memote's dedicated EGC test.
# Dissipation stoichiometry per currency (BiGG ids); genome-scale models also test GTP/CTP/UTP/q8h2.
DISSIPATION = {'atp': {'atp_c': -1, 'h2o_c': -1, 'adp_c': 1, 'pi_c': 1, 'h_c': 1},
'nadh': {'nadh_c': -1, 'nad_c': 1, 'h_c': 1},
'nadph': {'nadph_c': -1, 'nadp_c': 1, 'h_c': 1}}
def energy_generating_cycles(model, dissipations=DISSIPATION):
'''Max free-charging flux per currency with ALL uptake closed; >0 => energy-generating cycle.'''
out = {}
with model:
for ex in model.exchanges:
ex.lower_bound = 0 # no nutrients at all
if 'ATPM' in model.reactions:
model.reactions.get_by_id('ATPM').lower_bound = 0 # remove the NGAM floor before testing
for name, stoich in dissipations.items():
if any(m not in model.metabolites for m in stoich):
continue # currency absent from this model
with model:
r = cobra.Reaction(f'EGC_{name}')
r.add_metabolites({model.metabolites.get_by_id(m): c for m, c in stoich.items()})
r.bounds = (0, 1000)
model.add_reactions([r])
model.objective = r
out[name] = model.slim_optimize() # 0/infeasible = OK; positive finite = cycle
return out
Goal: Add the fewest reactions from a universal database that let the model grow on a defined medium.
Approach: Set the medium and the biomass objective, then call gapfill (which minimizes added reactions to reach the objective at lower_bound). There is no demand argument; demand_reactions=False avoids adding demand reactions for every metabolite. Record and low-confidence-flag every added reaction.
from cobra.flux_analysis import gapfill
universal = cobra.io.read_sbml_model('universal_model.xml') # e.g. a BiGG universal model
solutions = gapfill(model, universal, lower_bound=0.05, demand_reactions=False, iterations=3)
for i, rxns in enumerate(solutions):
print(f'solution {i+1}: {[r.id for r in rxns]}') # alternative gap-fill sets
# Adding these forces growth; that is not evidence they are biologically present. Flag them.
def imbalance(reaction):
'''Return the element and charge imbalance of a reaction (empty dict + 0 charge if balanced).'''
mass = {}
charge = 0
for met, coef in reaction.metabolites.items():
if met.formula:
for element, n in met.elements.items():
mass[element] = mass.get(element, 0) + coef * n
if met.charge is not None:
charge += coef * met.charge
return {e: v for e, v in mass.items() if abs(v) > 1e-6}, charge
# This is a PER-REACTION element/charge check. It is distinct from stoichiometric CONSISTENCY --
# a whole-network LP (Gevorgyan 2008, what memote tests) that finds mass leaks without needing
# formulas. "All reactions mass-balanced" does not imply the network is stoichiometrically consistent.
# Exchange/demand/sink AND the biomass pseudo-reaction are intentionally imbalanced; skip them.
# Proton (H) imbalance at pH 7 is the most common real fix.
from cobra.util.solver import linear_reaction_coefficients
pseudo = set(model.boundary) | set(linear_reaction_coefficients(model)) # boundary + objective (biomass)
unbalanced = [(r.id, imbalance(r)) for r in model.reactions
if r not in pseudo and (imbalance(r)[0] or abs(imbalance(r)[1]) > 1e-6)]
def dead_ends(model):
'''Metabolites that can only be produced or only consumed (a network gap or wrong stoichiometry).'''
out = []
for met in model.metabolites:
produced = any(r.metabolites[met] > 0 for r in met.reactions)
consumed = any(r.metabolites[met] < 0 for r in met.reactions)
if not (produced and consumed):
out.append(met.id)
return out
| Symptom | Cause | Fix |
|---------|-------|-----|
| "High memote score, so the model is good" | score measures consistency/annotation, not prediction | validate against measured growth/essentiality separately |
| Growth is huge; ATP looks free | erroneous energy-generating cycle | run the max-ATP-with-no-uptake test; constrain the offending reactions' directionality |
| gapfill(... demand=...) TypeError | there is no demand argument | set the objective and use lower_bound=/demand_reactions=False |
| memote.suite.api.run/snapshot AttributeError | wrong names | use test_model / snapshot_report |
| Many reactions flagged unbalanced | protons/charge at pH 7, or exchange reactions counted | skip exchange/sink/demand; fix H and charge first |
| Model still mispredicts after high score | consistency fixed, biology not validated | compare to Biolog carbon sources and an essentiality screen on the matched medium |
tools
End-to-end CLIP-seq pipeline from FASTQ to ENCODE-compliant binding sites, single-nucleotide crosslink maps, annotation, motifs, and (optionally) differential binding. Use when running the full Yeo lab eCLIP / iCLIP / iCLIP2 / iCLIP3 / irCLIP / PAR-CLIP analysis with SMInput control, protocol-specific UMI extraction, ENCODE STAR parameters, CLIPper or Skipper peak calling with stringent log2 FC and -log10 p thresholds, IDR rescue and self-consistency QC, and downstream motif registration with mCross or PEKA.
development
Detect, date, and contextualize whole-genome duplication (WGD / paleopolyploidy) events using wgd v2 (Chen et al 2024), KsRates (Sensalari 2022 substitution-rate-corrected Ks dating), DupGen_finder (Qiao 2019), MAPS (Li 2018 phylogenomic), POInT (Conant 2008 ordered-block), SLEDGe (2024 ML-based), Whale.jl (Bayesian DL+WGD), and synteny-anchored paranome construction. Use when identifying ancient polyploidy from Ks distributions and synteny block analysis, positioning WGD events relative to speciation, distinguishing tandem from segmental from WGD duplications, dating the 2R/3R vertebrate / fish / salmonid WGDs, building paranome and Ks-age mixture models, applying KsRates substitution-rate correction across lineages, or testing alternative biased-fractionation / dosage-balance models post-WGD.
tools
Build whole-genome alignments using Progressive Cactus (Armstrong 2020 reference-free clade-level WGA), Minigraph-Cactus (Hickey 2024 pangenome-aware), LASTZ chain/net (UCSC pipeline), MUMmer4 (Marçais 2018 pairwise), minimap2 -x asm5/10/20 (Li 2018 fast pairwise), AnchorWave (Song 2022 WGD-aware), and Mauve / progressiveMauve (bacterial). Operates the HAL toolkit (Hickey 2013) for downstream extraction including halSynteny, halLiftover, halBranchMutations, and hal2maf. Use when constructing multi-species alignments for comparative-annotation projection (TOGA), synteny detection, conservation analyses (phyloP / PhastCons), or pangenome graph construction; selecting between reference-free (Cactus) and reference-anchored (LASTZ chains/nets) approaches; tuning sensitivity for closely vs distantly related genomes; or producing HAL files for genome-wide downstream tools.
development
Detect syntenic blocks and structural rearrangements between genomes using MCScanX (Wang 2012), JCVI/MCScan (Tang 2008 Python), GENESPACE (Lovell 2022) for orthology-anchored riparian visualization, SyRI for structural variation, AnchorWave for sequence-level synteny, i-ADHoRe 3.0 for highly diverged species, SynNet for synteny networks, and ntSynt for multi-genome macrosynteny. Use when identifying collinear gene blocks across species, distinguishing macrosynteny from microsynteny, detecting inversions/translocations/duplications, anchoring orthology in WGD lineages, producing publication riparian plots, computing synteny block age via Ks (cross-references whole-genome-duplication), or running synteny-aware ortholog inference in polyploids.