# Gsmm Validator

> Validate a COBRApy genome-scale metabolic model for mass/charge balance, stoichiometric consistency, biomass producibility, dead-end metabolites, thermodynamic loops, and GPR rule formatting. Outputs a structured validation report with errors and warnings.

- Skill: `gabrielmoreira/gsmm-validator` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/gsmm-validator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/gsmm-validator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/gsmm-validator

---


## Overview

The `gsmm-validator` skill performs rigorous quality control on a COBRApy
`Model` before it enters any flux analysis pipeline. An invalid model
silently produces biologically meaningless fluxes; validation catches
structural errors early.

Validation covers six categories: (1) mass/charge balance, (2) feasibility
and biomass production, (3) dead-end metabolites, (4) stoichiometric
consistency, (5) thermodynamic loop detection, and (6) GPR rule integrity.

---

## Workflow

### Step 1 — Load the Model

```python
import cobra
import cobra.io

model = cobra.io.load_json_model("my_model.json")
print(f"Loaded: {model.id} ({len(model.reactions)} reactions)")
```

### Step 2 — Mass and Charge Balance Check

Unbalanced reactions are among the most common modelling errors. COBRApy
computes elemental balance per reaction.

```python
errors = []
warnings = []

print("=== Mass/Charge Balance ===")
for rxn in model.reactions:
    # Returns dict like {"C": -1, "H": 2} if imbalanced; empty dict if OK
    imbalance = rxn.check_mass_balance()
    if imbalance:
        # Exchange and demand reactions are expected to be imbalanced
        if rxn.id.startswith(("EX_", "DM_", "SK_", "BIOMASS")):
            warnings.append(f"WARN  [{rxn.id}] boundary reaction imbalanced "
                            f"(expected): {imbalance}")
        else:
            errors.append(f"ERROR [{rxn.id}] mass/charge imbalance: "
                          f"{imbalance}")

for msg in errors + warnings:
    print(msg)
print(f"  {len(errors)} error(s), {len(warnings)} warning(s)")
```

### Step 3 — Biomass Producibility (FBA Feasibility)

```python
print("\n=== Biomass Producibility ===")
solution = model.optimize()

if solution.status != "optimal":
    errors.append(f"ERROR Model is {solution.status} — "
                  f"cannot produce biomass under current medium.")
    print(f"  FAIL: {solution.status}")
elif solution.objective_value < 1e-6:
    errors.append("ERROR Growth rate is effectively zero "
                  "(< 1e-6 h^-1). Check medium and objective reaction.")
    print(f"  FAIL: growth = {solution.objective_value:.6f} h^-1")
else:
    print(f"  PASS: growth = {solution.objective_value:.4f} h^-1")
```

### Step 4 — Dead-End Metabolite Detection

A metabolite is a dead-end if it is produced by at least one reaction but
consumed by none, or vice versa. Dead-ends create infeasibility in network
regions.

```python
from cobra.manipulation import find_blocked_reactions

print("\n=== Dead-End Metabolites ===")
dead_end_mets = []

for met in model.metabolites:
    producers = [r for r in met.reactions
                 if r.get_coefficient(met) > 0]
    consumers = [r for r in met.reactions
                 if r.get_coefficient(met) < 0]

    if producers and not consumers:
        dead_end_mets.append((met.id, "produced but never consumed"))
    elif consumers and not producers:
        dead_end_mets.append((met.id, "consumed but never produced"))

if dead_end_mets:
    for met_id, reason in dead_end_mets:
        warnings.append(f"WARN  [{met_id}] dead-end: {reason}")
    print(f"  {len(dead_end_mets)} dead-end metabolite(s) found")
else:
    print("  PASS: no dead-end metabolites")

for msg in warnings[-len(dead_end_mets):]:
    print(f"  {msg}")
```

### Step 5 — Blocked Reaction Detection

```python
from cobra.flux_analysis import find_blocked_reactions

print("\n=== Blocked Reactions ===")
blocked = find_blocked_reactions(model, open_exchanges=True)

if blocked:
    warnings.append(f"WARN  {len(blocked)} blocked reaction(s): "
                    f"{blocked[:5]} ...")
    print(f"  {len(blocked)} blocked reactions (cannot carry flux)")
else:
    print("  PASS: no blocked reactions")
```

### Step 6 — Thermodynamic Loop Detection (Loopless FBA)

Energy-generating cycles violate thermodynamics and inflate apparent fluxes.

```python
print("\n=== Thermodynamic Loops ===")
try:
    loopless_solution = cobra.flux_analysis.loopless_solution(model)
    standard_solution = model.optimize()

    # Compare objective values; large discrepancy suggests loop inflation
    delta = abs(loopless_solution.objective_value
                - standard_solution.objective_value)
    if delta > 0.01:
        warnings.append(
            f"WARN  Loop detected: standard FBA growth "
            f"{standard_solution.objective_value:.4f} vs loopless "
            f"{loopless_solution.objective_value:.4f} (delta={delta:.4f})"
        )
        print(f"  WARN: possible thermodynamic loops (delta={delta:.4f})")
    else:
        print(f"  PASS: no significant loops (delta={delta:.6f})")
except Exception as exc:
    warnings.append(f"WARN  Loopless FBA failed: {exc}")
    print(f"  SKIP: loopless FBA unavailable ({exc})")
```

### Step 7 — GPR Rule Validation

Gene-Protein-Reaction associations must use valid gene IDs and boolean logic.

```python
import re

print("\n=== GPR Rule Integrity ===")
all_gene_ids = {g.id for g in model.genes}
gpr_errors = []

for rxn in model.reactions:
    gpr = rxn.gene_reaction_rule
    if not gpr:
        continue  # spontaneous or non-enzymatic reactions are fine

    # Extract gene IDs referenced in GPR
    referenced = set(re.findall(r"[A-Za-z0-9_\-\.]+", gpr))
    # Remove boolean keywords
    referenced -= {"and", "or", "not", "AND", "OR", "NOT"}

    missing = referenced - all_gene_ids
    if missing:
        gpr_errors.append(f"ERROR [{rxn.id}] GPR references unknown genes: "
                          f"{missing}")

if gpr_errors:
    errors.extend(gpr_errors)
    print(f"  {len(gpr_errors)} GPR error(s)")
else:
    print("  PASS: all GPR rules reference valid gene IDs")
```

### Step 8 — Write Validation Report

```python
import json
from datetime import datetime

report = {
    "model_id": model.id,
    "timestamp": datetime.utcnow().isoformat() + "Z",
    "n_reactions": len(model.reactions),
    "n_metabolites": len(model.metabolites),
    "n_genes": len(model.genes),
    "growth_rate": (solution.objective_value
                    if solution.status == "optimal" else None),
    "status": "FAIL" if errors else "PASS",
    "errors": errors,
    "warnings": warnings,
}

with open("validation_report.json", "w") as f:
    json.dump(report, f, indent=2)

print(f"\n=== Summary ===")
print(f"  Status   : {report['status']}")
print(f"  Errors   : {len(errors)}")
print(f"  Warnings : {len(warnings)}")
print("  Report written to validation_report.json")
```

---

## Key Conventions

| Check | Failure Condition | Severity |
|---|---|---|
| Mass balance | Non-exchange reaction has elemental imbalance | ERROR |
| Biomass producibility | `solution.status != "optimal"` or growth < 1e-6 | ERROR |
| Dead-end metabolites | Produced but never consumed (or vice versa) | WARNING |
| Blocked reactions | Reaction carries zero flux under all conditions | WARNING |
| Thermodynamic loops | Standard FBA growth >> loopless FBA growth | WARNING |
| GPR integrity | GPR string references gene IDs not in `model.genes` | ERROR |

### Interpretation Guide

- **ERROR** — must fix before proceeding to FBA. These cause incorrect results.
- **WARNING** — may indicate incomplete reconstruction; investigate per case.
- Boundary reactions (`EX_`, `DM_`, `SK_`, `BIOMASS`) are excluded from mass
  balance errors because they intentionally have no counter-reaction.
- A model with only warnings is acceptable for exploratory FBA but should be
  corrected for publication-quality analysis.

