Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,20 @@ jobs:
- name: Build release CLI
run: nim c -d:release --path:src -o:build/codonatlas src/codonatlas_cli.nim

- name: Smoke test the CLI
- name: Smoke test the FASTA path
shell: bash
run: |
BIN="build/codonatlas"
if [[ "$RUNNER_OS" == "Windows" ]]; then
BIN="${BIN}.exe"
fi
"./${BIN}" tests/data/sample.fasta >/dev/null

- name: Smoke test the FASTQ path
shell: bash
run: |
BIN="build/codonatlas"
if [[ "$RUNNER_OS" == "Windows" ]]; then
BIN="${BIN}.exe"
fi
"./${BIN}" tests/data/sample.fastq --quality=20 --kmer=3 >/dev/null
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

This project follows semantic versioning.

## [0.2.0] - 2026-08-03

Second coherent release. This release completes the FASTQ milestone from the
roadmap.

Added:

- FASTQ parsing with headers, descriptions, wrapped lines, and quality lines.
- Phred+33 quality score statistics per record.
- A FASTQ text report with per-record quality summaries.
- A `--format` option and automatic format detection by file extension.
- A `--quality` option to add a below-threshold count line.
- A bundled FASTQ fixture and a golden report comparison.
- Unit tests for the FASTQ parser and the quality statistics.

The package uses only the Nim standard library. It has no external
dependencies, so it needs no lockfile.

## [0.1.0] - 2026-08-02

First coherent release.
Expand Down
126 changes: 78 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# Codon Atlas

Codon Atlas is a Nim library for DNA and RNA sequence files. It computes
composition statistics. These include GC content, k-mer counts, and codon
usage tables. A command-line tool writes the results to a text report.
Codon Atlas is a Nim library for DNA and RNA sequence files. It reads FASTA
and FASTQ files. It computes GC content, k-mer counts, and codon usage tables.
It summarizes FASTQ quality scores. A command-line tool writes the results to
a text report.

## Features

- Read FASTA files with a permissive parser.
- Read FASTQ files with their quality lines.
- Compute GC content and per-base counts.
- Count overlapping k-mers.
- Build codon usage tables from the standard genetic code.
- Summarize FASTQ quality scores per record.
- Write a text report to the terminal or a file.
- Share one API across the library and the CLI.

Expand All @@ -31,6 +34,7 @@ Run the test suite:

```
nim c -r --path:src --outdir:build/tests tests/test_codonatlas.nim
nim c -r --path:src --outdir:build/tests tests/test_fastq.nim
```

On a checked-out repository, `nimble build` and `nimble test` are the
Expand Down Expand Up @@ -64,6 +68,25 @@ Show only the top 10 k-mers:
./build/codonatlas tests/data/sample.fasta --kmer=3 --top=10
```

Produce a report for a FASTQ file. The tool detects the format from the file
extension:

```
./build/codonatlas tests/data/sample.fastq
```

Add a quality threshold line to a FASTQ report:

```
./build/codonatlas tests/data/sample.fastq --quality=20
```

Force a format when the file extension is not standard:

```
./build/codonatlas reads.data --format=fastq
```

### Library

Import the package and build a report in code:
Expand All @@ -75,102 +98,109 @@ let records = readFasta("tests/data/sample.fasta")
echo buildReport(records, source = "sample.fasta")
```

Run the bundled example:
Build a report from a FASTQ file:

```nim
import codonatlas

let records = readFastq("tests/data/sample.fastq")
echo buildFastqReport(records, source = "sample.fastq",
options = ReportOptions(qualityThreshold = 20))
```

Run the bundled examples:

```
nim c -r --path:src --outdir:build/examples examples/demo.nim
nim c -r --path:src --outdir:build/examples examples/demo_fastq.nim
```

## Sample output

The command below prints a report for the bundled sample file:
The command below prints a FASTQ report for the bundled sample file:

```
./build/codonatlas tests/data/sample.fasta --kmer=3 --top=3
./build/codonatlas tests/data/sample.fastq --quality=20
```

```
Codon Atlas report
==================
Source: tests/data/sample.fasta
Generated: 2026-08-03T00:27:23Z
Source: tests/data/sample.fastq
Generated: 2026-08-03T02:15:15Z
Records: 2
Total canonical bases: 181
Total canonical bases: 35

Nucleotide composition
----------------------
Record 1: demo_orf
Description: synthetic coding sequence, 144 bp, starts with ATG and ends with TAA
Length: 144 canonical bases
Bases: A 40 C 31 G 38 T 35 U 0 N 0
GC content: 47.9%

Record 2: promoter_fragment
Description: synthetic non-coding fragment
Length: 37 canonical bases
Bases: A 9 C 8 G 9 T 11 U 0 N 0
GC content: 45.9%
Record 1: read1
Description: synthetic high-quality read, 23 bp
Length: 23 canonical bases
Bases: A 5 C 5 G 7 T 6 U 0 N 0
GC content: 52.2%
Quality: mean 38.3, min 37, max 40
Bases below Q20: 0

Record 2: read2
Description: synthetic low-quality read, 12 bp
Length: 12 canonical bases
Bases: A 3 C 4 G 2 T 3 U 0 N 0
GC content: 50.0%
Quality: mean 7.0, min 0, max 13
Bases below Q20: 12

Overall
-------
Total canonical bases: 181
GC content: 47.5%

K-mer counts (k = 3)
--------------------------------
Top 3 of 61 distinct k-mers.
CGT 8
GTA 7
GCG 6

Codon usage table (reading frame 0)
-----------------------------------
Total codons: 60

Count Frequency Per 1000 Codon AA Amino acid
--------------------------------------------
4 0.0667 66.7 CGU R Arginine
3 0.0500 50.0 AUG M Methionine
3 0.0500 50.0 GAC D Aspartic acid
3 0.0500 50.0 GAU D Aspartic acid
2 0.0333 33.3 AAG K Lysine
Total canonical bases: 35
GC content: 51.4%
```

The report lists all 64 codons of the standard genetic code. The output above
is abbreviated. Codons use RNA letters (U instead of T). A stop codon shows as
`*`.
Quality scores use the Phred+33 encoding. A higher score means a more
confident base call. A score of 20 is a common quality threshold.

The FASTA report adds a codon usage table. It lists all 64 codons of the
standard genetic code. Codons use RNA letters (U instead of T). A stop codon
shows as `*`.

## Project layout

```
codonatlas.nimble package metadata and tasks
src/codonatlas.nim public API entry point
src/codonatlas/fasta.nim FASTA parsing
src/codonatlas/fastq.nim FASTQ parsing
src/codonatlas/quality.nim quality score statistics
src/codonatlas/composition.nim GC content, base counts, k-mers
src/codonatlas/gencode.nim standard genetic code
src/codonatlas/codons.nim codon usage tables
src/codonatlas/report.nim text report builder
src/codonatlas/report.nim text report builders
src/codonatlas_cli.nim command-line interface
examples/demo.nim runnable example
examples/demo.nim runnable FASTA example
examples/demo_fastq.nim runnable FASTQ example
tests/ unit tests and fixtures
```

## Testing

Run `nimble check` to typecheck every module. Run `nimble test` to execute
the unit tests. The suite covers parsing, statistics, the genetic code, the
report builder, and a golden report comparison.
report builders, quality scores, and golden report comparisons.

## Limitations

- The parser keeps sequences as given. It does not reverse-complement.
- Codon counts use reading frame 0 on the given strands.
- Ambiguous bases (N and others) are excluded from composition ratios.
- The table normalizes codons to RNA form. It merges DNA and RNA input.
- The FASTQ parser assumes Sanger quality scores. It rejects sequences and
quality lines with different lengths.
- FASTQ reports do not include codon usage tables. Reads are usually short
unassembled fragments.

## Roadmap

Release 2 adds FASTQ support. It will summarize quality scores per record.
Release 2 is complete. Codon Atlas reads FASTQ files and summarizes quality
scores per record.

Release 3 adds translation. It will map codons to amino acids. It will also
detect open reading frames.
Expand Down
4 changes: 3 additions & 1 deletion codonatlas.nimble
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = "0.1.0"
version = "0.2.0"
author = "Codon Atlas contributors"
description = "Composition statistics for DNA and RNA sequence files"
license = "MIT"
Expand All @@ -10,8 +10,10 @@ requires "nim >= 1.6.0"

task test, "Run the unit test suite":
exec "nim c -r --path:src --outdir:build/tests tests/test_codonatlas.nim"
exec "nim c -r --path:src --outdir:build/tests tests/test_fastq.nim"

task check, "Typecheck all modules":
exec "nim check --path:src src/codonatlas.nim"
exec "nim check --path:src src/codonatlas_cli.nim"
exec "nim check --path:src tests/test_codonatlas.nim"
exec "nim check --path:src tests/test_fastq.nim"
11 changes: 11 additions & 0 deletions examples/demo_fastq.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Runnable example: build a Codon Atlas report for the bundled FASTQ file.

import std/os
import codonatlas

let samplePath = currentSourcePath().parentDir() / ".." / "tests" / "data" / "sample.fastq"
let records = readFastq(samplePath)

stdout.write buildFastqReport(records, source = "sample.fastq",
options = ReportOptions(kmer: 3, qualityThreshold: 20),
generatedAt = "2026-08-02T12:00:00Z")
9 changes: 5 additions & 4 deletions src/codonatlas.nim
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
## Codon Atlas: composition statistics for DNA and RNA sequence files.
##
## The package reads FASTA files, computes GC content and k-mer counts, and
## builds codon usage tables. A CLI writes the results to a plain-text report.
## The package reads FASTA and FASTQ files, computes GC content and k-mer
## counts, and builds codon usage tables. It summarizes FASTQ quality scores.
## A CLI writes the results to a plain-text report.

import codonatlas/[fasta, composition, gencode, codons, report]
import codonatlas/[fasta, fastq, quality, composition, gencode, codons, report]

export fasta, composition, gencode, codons, report
export fasta, fastq, quality, composition, gencode, codons, report
97 changes: 97 additions & 0 deletions src/codonatlas/fastq.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
## FASTQ reading and parsing.
##
## The parser accepts the standard Sanger FASTQ format. Each record has a
## header line, a sequence, a `+` separator, and a quality line. Sequence and
## quality lines may wrap over multiple lines. Their combined lengths must
## match. Quality scores use the Phred+33 encoding.

import std/strutils

type
FastqError* = object of CatchableError
## Raised when the FASTQ text is malformed.

FastqRecord* = object
id*: string
## First token of the header line, after the `@`.
description*: string
## Remainder of the header line, if any.
sequence*: string
## Concatenated sequence bases, with whitespace removed.
quality*: string
## Concatenated quality characters, with whitespace removed.

proc stripWhitespace(line: string): string =
result = newStringOfCap(line.len)
for ch in line:
if ch notin {' ', '\t', '\r', '\n'}:
result.add ch

proc parseHeader(line: string, lineNo: int): tuple[id, description: string] =
let header = line[1 .. ^1].strip
if header.len == 0:
raise newException(FastqError, "empty sequence header on line " & $lineNo)
let fields = header.splitWhitespace()
result.id = fields[0]
result.description = fields[1 .. ^1].join(" ")

proc parseFastq*(content: string): seq[FastqRecord] =
## Parse FASTQ text into a list of records.
##
## Raises `FastqError` when the text is malformed. An empty input produces
## an empty list.
let lines = splitLines(content)
var index = 0
while index < lines.len:
if lines[index].strip.len == 0:
inc index
continue
if lines[index][0] != '@':
raise newException(FastqError,
"expected a header starting with '@' (line " & $(index + 1) & ")")
var record = FastqRecord()
let (id, description) = parseHeader(lines[index], index + 1)
record.id = id
record.description = description
inc index

var sequenceParts: seq[string]
while index < lines.len:
let line = lines[index]
if line.len > 0 and line[0] == '+':
break
if line.len > 0:
sequenceParts.add stripWhitespace(line)
inc index
if sequenceParts.len == 0:
raise newException(FastqError,
"record '" & record.id & "' has no sequence")
if index >= lines.len:
raise newException(FastqError,
"record '" & record.id & "' is missing the '+' separator")
record.sequence = sequenceParts.join("")
inc index

if index >= lines.len:
raise newException(FastqError,
"record '" & record.id & "' has no quality line")
var quality = ""
while index < lines.len and quality.len < record.sequence.len:
if lines[index].len > 0:
quality.add stripWhitespace(lines[index])
inc index
if quality.len < record.sequence.len:
raise newException(FastqError,
"quality is shorter than the sequence in record '" & record.id & "'")
if quality.len > record.sequence.len:
raise newException(FastqError,
"quality is longer than the sequence in record '" & record.id & "'")
record.quality = quality
result.add record

proc readFastq*(path: string): seq[FastqRecord] =
## Read a FASTQ file and parse its records.
##
## Raises `IOError` when the file cannot be read. Raises `FastqError` when
## the file content is malformed.
parseFastq(readFile(path))
Loading
Loading