Bounded Count-Matrix QC and CPM Workflows
Build and inspect bounded RNA-seq count-matrix QC and optional counts-per-million (CPM) workflows, with an explicit human-review boundary.
Format: Hands-on lab. For readers with basic Python, CSV, and terminal experience: you will run and inspect a complete QC/CPM orchestrator against supplied synthetic count tables and tests; no biological dataset or API key is needed for the core lab.
Nasir Mahmood Abbasi, PhD
Bioinformatics Educator
What you will build
A small, bounded local R workflow validates a count matrix and matching sample metadata, computes sample-level quality summaries, optionally calculates descriptive counts per million (CPM), records input and script provenance, and stops for human review.
- Prerequisites: basic terminal and CSV experience, plus R 4.0 or later.
- Offline path: base R only. No package installation, account, API key, network request, or paid call is needed.
- Success: reproduce the supplied synthetic results, trigger a deliberate zero-library failure safely, and explain why software checks are not biological validation.
This R implementation is deterministic local code, not an autonomous scientist or an AI planner. It has a fixed allowlist of QC and CPM steps; it does not call a language model, execute generated code, remove samples, or infer research conclusions. The original Python workflow is retained below as an optional alternative, including its separate optional provider demonstration.
1. Download and set up the R workflow
Save the R workflow and its test file in the same new folder. Both use base R and are intended to run locally on the public synthetic fixtures. The R implementation writes the same main result names and QC/CPM columns as the retained Python example, but its manifest identifies files with base R's MD5 function. MD5 is a change-detection identifier here, not a security or authenticity guarantee.
R --version
Rscript orchestrator_workflow.R --help
If Rscript is missing, install R from CRAN. No R packages are required. Keep the offline exercise separate from real research data until you understand its limits.
2. Inspect the matching synthetic inputs
Rscript orchestrator_workflow.R --make-demo demo
This creates counts.csv, counts_positive.csv, and metadata.csv with the same values and headers as the existing Python workflow. The values are invented for teaching, not patient data or a downloaded GEO study. The output folder must not already exist.
gene_id,S1,S2,S3
GENE1,10,20,0
GENE2,0,5,0
GENE3,90,75,0
GENE4,0,0,0
The metadata header is exactly sample_id,group. The input validator requires matching unique sample IDs, simple identifiers, rectangular rows, and nonnegative integer counts. Metadata rows may be reordered because groups are joined by sample ID. This small implementation has explicit training limits of 2 MiB per file, 10,000 genes, and 100 samples; those are software limits, not biological thresholds.
3. Run QC and read the result
Rscript orchestrator_workflow.R --counts demo/counts.csv --metadata demo/metadata.csv --out run-qc
Open run-qc/sample_qc.csv and run-qc/report.md. The three sample totals should be S1: 100, S2: 100, and S3: 0; detected-gene counts are 2, 3, and 0. The default low-library warning threshold of 50 is only an exercise setting. It does not remove samples and is not a recommended RNA-seq cutoff. A zero-count sample is a valid QC input, but its source needs human investigation.
4. Run CPM and inspect the deliberate failure
Use the positive-control fixture for a successful descriptive CPM run:
Rscript orchestrator_workflow.R --counts demo/counts_positive.csv --metadata demo/metadata.csv --out run-cpm --task normalize
Each nonzero sample column in cpm.csv should sum to approximately one million. This is simple column-total scaling, not TPM, composition-bias correction, differential expression, or evidence of a biological effect.
Now request CPM for the fixture with an all-zero S3 column:
Rscript orchestrator_workflow.R --counts demo/counts.csv --metadata demo/metadata.csv --out run-zero-failure --task normalize
The command must fail with a nonzero status because dividing by a zero library total is undefined. It preserves the QC output and a failure event, but must not create cpm.csv or a successful report. Never invent replacement counts to make a real sample pass.
5. Run tests and inspect provenance
Rscript test_orchestrator_workflow.R orchestrator_workflow.R
The tests check the shared demo schema, known QC values, metadata reordering, valid CPM, zero-library refusal, invalid counts and metadata, overwrite protection, and provenance output. Each output directory records a fixed local plan in plan.json, input and script content identifiers and R version in manifest.json, and status events in events.jsonl. A completed report ends at NEEDS_HUMAN_REVIEW. A hash identifies bytes; it does not establish source authenticity, dataset suitability, or scientific correctness.
Inspect files, units, sample identities, warnings, and study design before interpreting any real dataset. The workflow is not a security sandbox, a production pipeline, or an independent biological reviewer. Its calculations are descriptive, and software tests do not validate a research conclusion.
Optional Python implementation and provider exercise
The tested Python workflow and its tests remain available as an alternative for readers who prefer Python. The following original Python instructions are retained. They use Python-native tools and are not needed to run the R workflow above. The AI planner described there is optional and makes a real provider request only if you explicitly run that separate command; the R implementation makes no provider call.
1. Download and set up
Save both files into the same new folder. The workflow file is the full implementation, not a pseudocode fragment; the second file contains its automated tests.
Open a terminal in that folder. On macOS or Linux:
python3 --version
python3 -m venv .venv
source .venv/bin/activate
python orchestrator_workflow.py --help
Windows PowerShell
py -3 --version
py -3 -m venv .venv
.\.venv\Scripts\python.exe orchestrator_workflow.py --helpUse .\.venv\Scripts\python.exe instead of python in the remaining commands. Activation is not required; do not weaken your execution policy just to activate a virtual environment.
Keep the offline lesson dependency-free. Only the optional provider integration below needs an extra package.
2. Inspect the synthetic inputs
python orchestrator_workflow.py --make-demo demo
This creates a new directory containing counts.csv, counts_positive.csv, and metadata.csv. All values are invented for teaching. These are not a patient cohort or a downloaded GEO study.
counts.csv deliberately contains one all-zero sample:
gene_id,S1,S2,S3
GENE1,10,20,0
GENE2,0,5,0
GENE3,90,75,0
GENE4,0,0,0
sample_id,group
S1,control
S2,case
S3,case
counts_positive.csv is a separate positive-control fixture: its S3 values are 5, 0, 45, and 0. It lets you test a valid normalization run without altering the zero-library example. Never invent replacement counts to repair a real failed sample.
The validator requires unique gene and sample identifiers, nonnegative integer counts, complete rectangular rows, and an exact sample-ID match between counts and metadata. Metadata order may differ: rows are joined by sample ID, never by their position. This teaching implementation accepts simple identifiers made from letters, numbers, underscores, dots, and hyphens; leading letters or digits are required. Count fields must contain 1–12 ASCII digits, with a maximum value of 999,999,999,999. It intentionally limits input size to 2 MiB per file, 10,000 genes, and 100 samples. These are software training limits, not biological thresholds.
3. Validate a plan before execution
The orchestrator has a deliberately small tool registry. For --task qc, the only valid plan is {"steps":["qc"]}. For --task normalize, it is {"steps":["qc","cpm"]}. Unknown tools, extra or duplicate JSON keys, repeated tools, and changed ordering are rejected. A plan file is limited to 16 KiB. There is no shell tool and no generated-code execution.
Start with a dry run:
python orchestrator_workflow.py --counts demo/counts.csv --metadata demo/metadata.csv --out run-plan --dry-run
Inspect run-plan/plan.json, the input manifest, and the event log. A dry run validates and records a plan but does not produce sample_qc.csv or cpm.csv. Every output directory must be new; the program refuses to overwrite a previous run.
The design is intentionally conservative: an AI planner cannot remove a sample, change a threshold, select a new tool, or claim a result. The command-line task and parameters remain human choices.
4. Run QC and read the result
python orchestrator_workflow.py --counts demo/counts.csv --metadata demo/metadata.csv --out run-qc
Open run-qc/sample_qc.csv and run-qc/report.md. You should find:
| Sample | Count total | Detected genes |
|---|---|---|
| S1 | 100 | 2 |
| S2 | 100 | 3 |
| S3 | 0 | 0 |
These totals cover only the supplied four-gene matrix; they are not estimates of sequencing depth. The default low-total warning threshold is 50 counts solely to exercise the software. It is not a recommended RNA-seq QC cutoff. Warnings do not silently remove samples.
The numerical reviewer checks the computed totals and detected-gene counts against the inputs. A completed run ends at NEEDS_HUMAN_REVIEW, not “biologically valid.” A zero-count sample is a valid software input for QC, but a warning requiring a person to investigate the source data.
5. Test successful CPM and a deliberate failure
First use the positive-control fixture:
python orchestrator_workflow.py --counts demo/counts_positive.csv --metadata demo/metadata.csv --out run-normalized --task normalize
Here the S3 total is 50, with two detected genes. Each sample column in cpm.csv should sum to approximately one million. The calculation is:
CPM(gene, sample) = count(gene, sample) / column_total(sample) * 1,000,000
The reviewer checks nonnegative finite CPM values and their column sums. This descriptive scaling is not TPM, composition-bias correction, differential expression, or proof of a biological effect. Its denominator depends on the genes in the supplied matrix. Do not interpret the toy case/control labels as an experimental comparison.
Now deliberately request normalization of the zero-library fixture:
python orchestrator_workflow.py --counts demo/counts.csv --metadata demo/metadata.csv --out run-zero-failure --task normalize
This command must fail with a nonzero exit status: dividing by a zero count-column total is undefined. Its QC output and failure event remain available for inspection, but it must not create a successful normalization report or cpm.csv. A useful orchestrator makes this failure explicit instead of hiding the sample or manufacturing a denominator.
6. Add an optional AI planner
Only attempt this after the offline commands and tests pass. Install the optional SDK with the same virtual-environment interpreter used to run the workflow. After activation, use:
python -m pip install openai
On Windows, where the earlier instructions do not activate the environment, use .\.venv\Scripts\python.exe -m pip install openai. On macOS or Linux without activation, use .venv/bin/python -m pip install openai. This keeps the SDK in the interpreter that will execute the script; if installation reports an error, stop and resolve it before trying the provider command.
Configure OPENAI_API_KEY in your environment using your provider's instructions. Select a model available in your own account that supports Chat Completions with JSON-schema structured output. Replace YOUR_MODEL_ID below; it is a placeholder, not a working model name.
python orchestrator_workflow.py --counts demo/counts_positive.csv --metadata demo/metadata.csv --out run-ai --task normalize --planner openai --model YOUR_MODEL_ID
This path makes at most one model request, with a 30-second request timeout and no automatic retries. Provider charges may apply. It sends only the selected task and generic matrix dimensions, not gene IDs, sample IDs, group labels, or raw counts. The returned JSON still passes the same local allowlist and exact-plan checks as an offline plan. A refusal, malformed response, or provider failure stops the run; there is no hidden fallback claiming that AI succeeded.
For an explicitly configured OpenAI-compatible endpoint, the SDK also accepts OPENAI_BASE_URL. Check that provider's schema support first. The offline test suite mocks the provider adapter; that is not a guarantee of compatibility with every live model, account, SDK release, or endpoint.
Why such a narrow planner? A constrained first project lets you demonstrate an important boundary: the model may propose data, but trusted code decides which actions are allowed. Add tools only after defining their schemas, budgets, test cases, and approval points.
For these two fixed plans, an LLM offers no analytical advantage over the offline planner. Its optional role here is to teach the provider/validation boundary, not to make simple arithmetic appear intelligent. Prefer the offline run unless you specifically want to test that integration.
7. Inspect provenance and review limits
plan.jsonrecords the actual accepted steps.manifest.jsonrecords input and script SHA-256 hashes, Python version, task, parameters, and planner mode. Hashes identify file contents; they do not prove a dataset is scientifically suitable.events.jsonlrecords the roles and success/failure states. A failed run keeps its failure record when an output directory has been created.sample_qc.csv, optionalcpm.csv, andreport.mdseparate numerical outputs from the human-review decision.
Before accepting any output, a person must confirm that sample identities and units are correct, warnings have been investigated, the chosen analysis answers the research question, and no result is being inferred beyond what the data support. The numerical reviewer is a second implementation check, not an independent biological expert.
This is not a security sandbox or a production workflow engine. It runs with your Python process's normal permissions. Its small input caps, fixed tool registry, new output directories, and absence of generated-code execution reduce the teaching example's scope; they do not make arbitrary untrusted software safe.
For a real study, first inspect the specific files and document how they were generated. A GEO accession or Series record does not guarantee an analysis-ready raw-count matrix: GEO distinguishes raw sequencing reads, submitter-supplied processed files, metadata, and, for some studies, NCBI-generated count matrices. Check the file description and units before using it. Do not feed log-normalized expression into this integer-count example or truncate a real matrix silently to pass its training limits. GEO's official FAQ describes these data types.
8. Run tests and practice
python -m unittest -v test_orchestrator_workflow.py
The downloadable tests exercise known QC values, metadata reordering, positive CPM, the zero-library refusal, invalid counts and schemas, blocked plans, dry runs, overwrite protection, and a mocked provider call. Tests check what this small implementation promises, not the accuracy of a scientific conclusion.
Troubleshooting
- Output directory already exists: choose a new run name. Do not delete prior evidence just to make a rerun pass.
- Counts rejected: check for missing fields, duplicate identifiers, non-integer values, or negative counts. Confirm you exported raw counts, not transformed expression.
- Sample mismatch: compare identifiers exactly. Reordering metadata is allowed; inventing or silently dropping samples is not.
- Zero-library normalization failure: inspect the original data and QC warning. Do not add artificial counts merely to bypass the check.
- Optional provider failure: verify the SDK, model ID, account access, environment configuration, and schema support. Return to a clearly labeled offline run rather than silently treating it as AI output.
Practice with an observable result
- Reorder the three metadata rows in a copy. Rerun QC into a new directory. Sample identities and metrics should remain the same.
- Create a plan file containing
{"steps":["qc","run_shell"]}and supply it with--plan blocked-plan.json. The plan must be rejected before tool execution. - Add a duplicate gene or a fractional count to a copied input. Explain which validator rejects it and why silently coercing the value would be unsafe.
- Compare two offline runs of unchanged inputs. Match their hashes and numerical outputs; distinguish reproducibility of computation from validity of interpretation.
Check your understanding
Why can QC succeed when S3 has zero counts? Computing and flagging the zero total is valid; normalizing by that total is not. Does a valid model plan establish scientific correctness? No. It establishes only that the proposed tool sequence satisfies the local contract. What is the human approval point? Reviewing the evidence before using or reporting an analysis conclusion; the script never declares that approval on your behalf.
References and further learning
- Python CSV documentation: file parsing and its explicit conversion requirements.
- Python argparse documentation: CLI options, validation, and mutually exclusive arguments.
- OpenAI structured-output guidance: schemas, refusals, and incomplete responses; a schema is not a substitute for application validation.
- edgeR CPM reference: descriptive CPM and the distinction between simple matrix column sums and normalized library sizes.
Continue learning
Explore LangGraph, AutoGen, and foundation-model orchestration or return to the AI tutorial category.