MinCED
Identify CRISPR spacers in FASTA sequences and match them against viral databases to infer host–virus interactions.
Installation
We first need to clone the GitHub repository of the program. Go to github.com/ctSkennerton/minced, click the green Code button and copy the HTTPS address. Then enter the following command in the location you want to install the program (there is no need to create a new folder):
git clone https://github.com/ctSkennerton/minced.git
Inside the folder where the repo was cloned, run:
make
In case some Java components are missing, install them:
sudo apt-get install default-jdk
Alternative installation
conda install bioconda::minced
Running
Import the folder containing the program to the PATH
export PATH="/home/minced:/$PATH"
Always keep the files minced and minced.jar in the same folder.
When looking for CRISPR, always use a nucleotide sequence as input.
./minced -gffFull -spacers Genome.fna CRISPR_Genome.txt CRISPR_Genome.gff
Three files will be generated:
| File | Description |
|---|---|
| CRISPR_Genome.gff | GFF annotation file for the detected CRISPR arrays |
| CRISPR_Genome.fa | FASTA sequences of the spacers and the scaffold they came from |
| CRISPR_Genome.txt | Position and length of the spacers and repeats |
Useful commands
./minced --help
MinCED, a program to find CRISPRs in shotgun DNA sequences or full genomes
Usage: minced [options] file.fa [outputFile.txt] [outputFile.gff]
Options: -searchWL Length of search window used to discover CRISPRs (range: 6-9). Default: 8
-minNR Minimum number of repeats a CRISPR must contain. Default: 3
-minRL Minimum length of the CRISPR repeats. Default: 23
-maxRL Maximum length of the CRISPR repeats. Default: 47
-minSL Minimum length of the CRISPR spacers. Default: 26
-maxSL Maximum length of the CRISPR spacers. Default: 50
-gff Output summary results in gff format containing
only the positions of the CRISPR arrays. Default: false
-gffFull Output detailed results in gff format containing
positions of CRISPR arrays and all repeat units. Default: false
-spacers Output a fasta formatted file containing the spacers. Default: false
-h --help Output this handy help message
--version Output version information
./minced --version
Identifying Viruses Matching the Spacers
Create a BLASTn database
Create a BLASTn database with the dataset you want to search for spacer matches:
makeblastdb -in IMGVR_viral_genomes.fna -dbtype nucl -parse_seqids
Running BLASTn against database
Use the blastn-short task — better suited for short sequences like spacers. The spacers are used as the query, and the viral dataset as the database.
blastn -task blastn-short -query minced-spacer.fa -db IMGVR_viral_genomes.fna -word_size 7 -perc_identity 100 -qcov_hsp_perc 100 -ungapped -dust no -soft_masking false -evalue 1000 -num_threads 10 -max_hsps 1 -outfmt "6 qseqid sseqid pident length mismatch gapopen qstart qend sstart send sstrand evalue bitscore" -out Blast_spacers_vs_IMGVR.tab
Running BLASTn against a genome
To check if your spacers match a specific genome, use the -subject flag instead of creating a database:
blastn -task blastn-short -query minced-spacer.fa -subject target_Genomes.fna -word_size 7 -perc_identity 100 -qcov_hsp_perc 100 -ungapped -max_target_seqs 100 -outfmt "6 qseqid sseqid pident length mismatch gapopen qstart qend sstart send sstrand evalue bitscore" -num_threads 10 -out Blast_spacers_vs_genome.tab
Curating the Results
Acquire spacer lengths
First we need the spacer length information. Run this on the FASTA file containing the spacer sequences:
seqkit fx2tab --length --name --header-line minced-spacer.fa >> minced-spacer_lenght.tab
Length of spacers and matches
Now we can check features of the BLASTn results to confirm true positives. The following script checks these parameters:
| Parameter | Criteria |
|---|---|
| Full-length alignment | Alignment length equals spacer length (perfect) or shorter by 1 nt (ok) |
| Number of mismatches | 0 mismatches (perfect) or 1 mismatch (ok) |
| No gaps | gapopen = 0 |
#!/usr/bin/env python3
"""
Filter BLASTn spacer hits using spacer lengths + quality rules.
Automatically adds BLAST header if missing.
Expected BLAST outfmt:
6 qseqid sseqid pident length mismatch gapopen qstart qend sstart send sstrand evalue bitscore
"""
import argparse
import sys
import pandas as pd
from pathlib import Path
BLAST_HEADER = [
"qseqid", "sseqid", "pident", "length", "mismatch", "gapopen",
"qstart", "qend", "sstart", "send", "sstrand", "evalue", "bitscore"
]
def ensure_blast_header(blast_path: Path):
"""
Ensure BLAST outfmt 6 file has the correct header.
If missing, prepend it in-place.
"""
with blast_path.open() as f:
first_line = f.readline().strip()
# If header already present, do nothing
if first_line.split("\t") == BLAST_HEADER:
return
# Otherwise, prepend header
tmp_path = blast_path.with_suffix(".tmp")
with blast_path.open() as fin, tmp_path.open("w") as fout:
fout.write("\t".join(BLAST_HEADER) + "\n")
for line in fin:
fout.write(line)
tmp_path.replace(blast_path)
def read_spacer_lengths(path: str) -> pd.DataFrame:
df = pd.read_csv(
path,
sep="\t",
comment="#",
header=None,
names=["qseqid", "spacer_len"],
dtype={"qseqid": str}
)
df["spacer_len"] = pd.to_numeric(df["spacer_len"], errors="coerce")
df = df.dropna(subset=["spacer_len"]).copy()
df["spacer_len"] = df["spacer_len"].astype(int)
return df
def read_blast(path: str) -> pd.DataFrame:
df = pd.read_csv(path, sep="\t", dtype=str)
numeric_cols = ["pident", "length", "mismatch", "gapopen", "qstart",
"qend", "sstart", "send", "evalue", "bitscore"]
for col in numeric_cols:
df[col] = pd.to_numeric(df[col], errors="coerce")
return df.dropna(subset=["length", "mismatch", "gapopen"])
def classify_len(aln_len, spacer_len):
if aln_len == spacer_len:
return "perfect"
if aln_len == spacer_len - 1:
return "ok"
return None
def classify_mismatch(mm):
if mm == 0:
return "perfect"
if mm == 1:
return "ok"
return None
def main():
ap = argparse.ArgumentParser(
description="Add BLAST header (if missing) and filter spacer BLAST hits."
)
ap.add_argument("-b", "--blast", required=True, help="BLAST outfmt 6 file")
ap.add_argument("-l", "--lengths", required=True, help="Spacer lengths table")
ap.add_argument("-o", "--out", default="-", help="Output TSV (default: stdout)")
ap.add_argument("--reason-col", default="selection_reason",
help="Name of the added reason column")
args = ap.parse_args()
blast_path = Path(args.blast)
# 1) Ensure header exists
ensure_blast_header(blast_path)
# 2) Read inputs
blast = read_blast(blast_path)
lengths = read_spacer_lengths(args.lengths)
df = blast.merge(lengths, on="qseqid", how="inner")
# 3) Apply filters
df["len_class"] = df.apply(
lambda r: classify_len(int(r["length"]), int(r["spacer_len"])), axis=1
)
df["mm_class"] = df["mismatch"].apply(classify_mismatch)
passed = df[
(df["gapopen"] == 0) &
(df["len_class"].isin(["perfect", "ok"])) &
(df["mm_class"].isin(["perfect", "ok"]))
].copy()
# 4) Build reason column
passed[args.reason_col] = (
"len:" + passed["len_class"] +
";mismatch:" + passed["mm_class"] +
";gap:perfect"
)
# 5) Output: BLAST columns + reason
out_cols = BLAST_HEADER + [args.reason_col]
passed = passed[out_cols]
if args.out == "-" or args.out.lower() == "stdout":
passed.to_csv(sys.stdout, sep="\t", index=False)
else:
passed.to_csv(args.out, sep="\t", index=False)
if __name__ == "__main__":
main()
Transform this script into an executable:
chmod +x Spacer-match_check.sh
Run the script to generate your final output:
python3 Spacer-match_check.sh -b Blast_spacers_vs_genome.tab -l minced-spacer_lenght.tab -o Checked_Blast_spacers_vs_genome.tab
Extra Thoughts
Spacers belonging to a CRISPR array
If a scaffold has a 100% match and 100% query coverage (qend − qstart) with a spacer, there are two options:
- This assembly belongs to a MGE
- This assembly is part of a CRISPR array
There are 2 ways to check this:
- See if that scaffold was used in the binning process to form a MAG — if it belongs to a MAG it is likely from a CRISPR array (but not definitive, it can also be a provirus).
- Run MinCED on these scaffolds to see if a CRISPR array organisation is detected.
If the scaffolds do not belong to a MAG and no CRISPR array is detected, treat them as possible MGEs and proceed to the virus annotation pipeline.