Five C++20 engines for single-cell and epigenomic data, built so the analysis runs on the machine that already holds the sequencing data.
pip install pto-corepto-core doctordoctor never touches your data. It reports which of the vendored engines
this wheel actually resolves, where each one came from (bundled inside the
wheel versus a PATH or PTO_BIN_DIR override), and the version each one
reports back — a wheel that can't clear this is broken, not slow, and this is
the one command to run before spending evaluator time on the rest.
pto-core qc your_reads.fastq.gzThat runs the streaming QC path (fastq_stream --qc-only) against a real
file and prints the summary — read counts, Q30 rate, mean quality — with no
build step, no CMake, and no cluster job. pto-core man lists every other
command the same way; pto-core man frip (or any other) prints one command's
parameters.
pto-core ships four of the five engines below — fastq_stream,
genomic_toolkit, cuttag_profiler, and pto-peaks — as one wheel with zero
Python runtime dependencies, and nothing left for the host to provide at the
system level either: htslib and libdeflate are already linked into the
binaries. The install this package exists to make frictionless is landing on
a login node or a shared workstation without touching whatever numpy or
pydantic version the site has already pinned, and without a compiler in
sight.
scrna_matrix is not in that wheel, and the reason is the same one that
makes its own CMake configure step a hard failure without OpenMP: a silently
single-threaded fallback that still reports success is worse than a build
that stops (see OpenMP is mandatory,
below). Bundling that requirement into pto-core would mean a macOS host
without libomp could not install the wheel at all, just to get a FRiP
number out of genomic_toolkit. So it is published on its own, as a compiled
extension module rather than a CLI:
pip install pto-core scrna-matrixReach for both when the workflow needs a k-NN graph over an expression
matrix; pto-core by itself is enough for QC, fragment metrics, and CUT&Tag
profiling.
The modules were developed independently and are vendored here so they can be built, tested and reviewed together. Each remains a self-contained CMake project with its own dependencies, options, tests and install rules, and each must stay independently buildable with the other four deleted.
| Module | What it does | Language surface |
|---|---|---|
modules/scrna_matrix |
Cache-line-aligned sparse matrix engine (Block_CSR<T>), SIMD sparse-dot and cosine kernels, exact and HNSW k-NN |
Header-only C++20 plus zero-copy pybind11 bindings |
modules/fastq_stream |
Streaming FASTQ QC, adapter and quality trimming, piped straight into an aligner | C++20 library and CLI |
modules/cuttag_profiler |
CUT&Tag and CUT&RUN reference-point signal matrices, with an embedded loopback dashboard | Single C++20 binary |
modules/genomic_toolkit |
Fragment size distributions, duplicate rate and FRiP in one streaming pass, with no sort and no intermediate files | C++20 library and CLI |
modules/peaks |
pto-peaks: single-pass pileup, multi-scale rolling background, SIMD Poisson tails, Benjamini-Hochberg, narrowPeak out |
C++20 headers and CLI |
What unifies these is a deployment constraint rather than a shared abstraction: each tool has to run on the cluster or workstation where the data already lives, with no service dependency and no data leaving the host. There is no shared runtime between the modules, and none is intended. See Repository layout for why that absence is a decision rather than an omission.
There is no repository-wide document explaining how any one engine works
internally, in keeping with the "no shared layer" decision above — that lives
in each module's own README and, where one exists, its docs/ARCHITECTURE.md
(fastq_stream has one). Architecture & System
Design, further down, covers the other kind of
architecture: how the five are built, gated and packaged together.
One command runs three of the five modules end to end and serves the dashboard:
./scripts/run_demo.shIt is offline by default, with fixtures checked in or synthesised locally. See
docs/DEMO.md for options, deployment and troubleshooting.
The deployment kit reaches the dashboard over an SSH tunnel rather than a public
port. cuttag_profiler requires a per-run session token, but that token is one
shared secret for the run and not a login system; see
deploy/README.md for the actual security model. An
internet-facing port 8080 is unsupported.
ctest at the top level runs thirty-two suites, or thirty-three when
pybind11 is available and the Python binding tests register.
| Test | Module | Covers |
|---|---|---|
test_matrix_ops |
scrna_matrix | Block_CSR invariants, SIMD parity against a double reference across every vector-tail boundary, exact k-NN, HNSW recall |
test_wide_accumulation |
scrna_matrix | scans the module's own sources and fails the build on a bare float reduction accumulator |
test_hnsw_persist |
scrna_matrix | HNSW serialization is lossless: a reloaded index returns byte-identical neighbour lists, not merely approximately equal ones |
test_python_bindings* |
scrna_matrix | buffer adoption, lifetime and keepalive, GIL behaviour, NumPy and SciPy round-trips |
trimmer |
fastq_stream | SIMD kernels, lock-free ring, quality and adapter trimming, record framing, end-to-end pipeline over raw, gzip and BGZF |
fastq_stream_cli |
fastq_stream | the binary as a black box: argument validation, report files, non-regular-file inputs |
bam_query |
cuttag_profiler | BAM iteration, filtering, index handling |
signal_calc |
cuttag_profiler | window construction, normalisation, strand orientation, thread-count invariance, BED and GTF parsing |
matrix_cache |
cuttag_profiler | the cached-matrix memory bound: a cache capped at a fixed entry count is not a memory bound when nothing caps a region count |
interval_index |
cuttag_profiler | the pruning descent, differentially checked against an O(n) scan over 20k random queries |
secrets |
cuttag_profiler | session-token minting from the CSPRNG and constant-time comparison |
region_names |
cuttag_profiler | region-name handling from the HTTP security audit |
bed_reader |
cuttag_profiler | bounded line growth and other parsing behaviour |
file_identity |
cuttag_profiler | an output path may not name an input |
profiler_cli |
cuttag_profiler | the binary as a black box |
server_linkage |
cuttag_profiler | asserts against the linked artefact, in both directions, whether the embedded HTTP server is present |
http_security |
cuttag_profiler | Host and Origin validation: loopback names, IPv6 literals, port mismatch, and the substring tricks a find-based check would accept |
gtk_peak_set |
genomic_toolkit | interval merging, half-open boundaries, coordinate ceiling, differential test against a per-base bitmap |
gtk_dup_marker |
genomic_toolkit | sorted-stream and hash duplicate modes agreeing on the same set, the O(pile-up) memory claim, out-of-order and contig-revisit detection |
gtk_fragment_stream |
genomic_toolkit | BEDPE pairing, size and MAPQ filters, malformed-line accounting, CRLF, over-long lines, batching boundaries |
gtk_frip |
genomic_toolkit | end-to-end FRiP, the overlapping-peak double-count trap, duplicate exclusion, chromosome-naming mismatch |
gtk_fuzz_parsers |
genomic_toolkit | seeded mutation fuzzing of the BEDPE and fragment-BED parsers, plus emitted-fragment invariants |
gtk_file_identity |
genomic_toolkit | --histogram may not name an input, its index, or the peak file |
gtk_cli_args |
genomic_toolkit | the checked integer parse behind every numeric option: partial numbers, int32 narrowing, int64 overflow, per-option ranges |
gtk_cli |
genomic_toolkit | the binary as a black box: exit codes, refusals, truncated-BAM cases |
gtk_region_partition |
genomic_toolkit | htslib only: --region tiles partition a file exactly |
gtk_bam_contig_order |
genomic_toolkit | htslib only: FRiP on a multi-contig BAM does not depend on the peak file's contig order |
peaks_poisson_model |
peaks | Poisson tails against mpmath golden values, the degenerate-background fail-closed rule, the finite-lower-sum fast-path gate, convergence at the iteration ceiling |
peaks_sliding_window |
peaks | the centred multi-scale background rings, contig-end truncation, the coverage-desert skip, and back pressure |
peaks_caller |
peaks | the state machine, the float-comparison summit-plateau fix, Benjamini-Hochberg's cutoff-relative floor, batch-size invariance |
peaks_bam_streamer |
peaks | native BGZF and BAM decoding by byte shift, per-block CRC32, an empty BGZF block mid-stream staying transparent |
peaks_file_identity |
peaks | an output path may not name an input, including chrom-sizes and index files |
peaks_cli |
peaks | the binary as a black box: argument parsing, the BED reader's fail-closed checks, the narrowPeak writer, and the exit codes the job contract depends on |
* Registered only when CMake finds pybind11, and skipped with a
configure-time message otherwise.
The C++ suites are dependency-free by design, with no GoogleTest and no Catch2, for the same reason the tools are: they must run on a cluster that has a compiler and nothing else.
Sanitizer builds are per-module rather than wired into the default build. For
fastq_stream:
cmake -S modules/fastq_stream -B build/fq-asan -DFQ_SANITIZE=ON -DFQ_NATIVE=OFF
cmake --build build/fq-asan -j && ./build/fq-asan/test_trimmerNo macOS toolchain combines ThreadSanitizer with OpenMP: Apple Clang has no
OpenMP, and Homebrew GCC has no sanitizer runtimes on arm64. Use the container,
which pins clang, libomp-dev and libarcher:
docker build -f docker/Dockerfile.tsan -t pto-tsan . && docker run --rm pto-tsanscripts/run_tsan.sh sweeps thread counts and repeats, and refuses to run
rather than "verifying" a serial build when the toolchain cannot supply
-fopenmp -fsanitize=thread. fastq_stream and cuttag_profiler are
std::thread-based and need no container.
Two sanitizer notes worth knowing before you reproduce an audit. A UBSan
suppression is inert under -fno-sanitize-recover=all, so use
-fno-sanitize=alignment for hnswlib's known misaligned store. And
AlignedAllocator rounds allocations up to 64 bytes, so an ASan overread
reproduction needs posix_memalign at the exact size.
The suites above prove each module does what its author intended on fixtures
its author wrote. tests/validation/ answers the other
question: on real public sequencing runs, does pto-core produce the same
answers as the tools it replaces?
Those are not the same question. A synthetic FASTQ has no adapter dimers, no quality cliff at cycle 140 and no duplicate structure. A synthetic BAM has no soft-clipping and no unplaced contigs. Every interesting disagreement between two tools lives in exactly that material.
tests/validation/run_validation.py list # what can run here
tests/validation/run_validation.py fetch --allow-download # stage inputs
tests/validation/run_validation.py runEleven cases across four modules: TSS meta-profiles against deeptools
(Pearson r at least 0.99), FRiP against samtools and bedtools (exact count),
fragment sizes against samtools TLEN (KS at most 0.02), QC counters against
seqkit (exact), trimming against fastp, alignment rate through bowtie2,
duplicate rate against Picard, and HNSW recall against scrna_matrix's own
exact path. Datasets are real pinned accessions (ENCODE ENCSR000AKA, ENA
PRJNA512492) verified by checksum.
Two properties are load-bearing rather than stylistic. Network access is
opt-in: without --allow-download nothing touches the internet, and a missing
input is a clean skip carrying the command to stage it. These tools run where
the data is, and that host is the last place a test harness should be reaching
out from. And skips are not failures, but a case with no metrics is an error.
Silently passing because every comparison was skipped is the most dangerous
thing a validation suite can do, and it is the default if you only check for
failures.
Where exact equality is not expected, the reason is documented next to the
threshold rather than absorbed into a comfortable tolerance, and both observed
values are printed on every run so that the gap changing is visible.
markdup against Picard compares aligned span to unclipped 5' end;
cuttag_profiler against deeptools compares span-overlap to midpoint binning.
Measured on an Apple M4 Pro with 12 cores and Homebrew GCC 16. See
docs/PERFORMANCE_2026-08-15.md for the full
report, the exact commands, and what was not measured. The cross-tool Pareto
table, which is the number to read if you are deciding whether to adopt any of
this, is the one directly below.
| Module | Headline | Against the baseline |
|---|---|---|
fastq_stream |
16.6 M reads/s, 4,954 MB/s at 12 threads; constant 10 MB peak RSS | Peak RSS does not grow with input size at all |
scrna_matrix |
1.3M cells in 37.1 s at 1.01 GB peak RSS (HNSW); NEON gives 2.02x on brute-force k-NN | See the caveat below |
cuttag_profiler |
100k windows in 1.59 s at 88.9 MB | 32.6x faster and 4.7x less memory than deeptools, at r = 0.999991 |
genomic_toolkit* |
400k fragments scored in 0.05 s, no intermediate files | 28x faster than the samtools-sort, bamtobed, sort, intersect chain, with an identical in-peak count of 148,680 |
* genomic_toolkit is not part of the 2026-08-15 performance run. It postdates
it, and its row was measured separately with Apple Clang on the repository's own
demo.bam. Reproduce both sides, counts included, with
modules/genomic_toolkit/scripts/validate_against_bedtools.sh.
The scrna_matrix row is feasibility, not superiority. The 1.3M-cell figure
is a real run including index construction and all 1.3M queries, and the exact
path extrapolates to roughly 1.6 hours on the same data at about 3.5 ns per
n-squared, which is a projected 155x. But there is no like-for-like
scanpy.pp.neighbors run at 1.3M cells, so no comparative claim is made at that
scale. A 50,000-cell head-to-head does exist in the performance report, and even
that carries two caveats stated there rather than buried: numba JIT warm-up
dominates a single first call, and a graph built from X against scanpy's
default compares two different computations. sc.pp.neighbors on a PCA
representation is itself approximate and near-linear, which makes it a peer of
the HNSW backend rather than a baseline it should categorically beat.
peaks has no row here at all. It has not been benchmarked for speed
against MACS3, only for concordance, and on that axis this project's own 0.98
parity gates are not met. Summit location agrees to a 1 bp median offset with
97.6% of matched summits inside 50 bp, and peak strength correlates at Pearson
r 0.990, but pto-peaks calls intervals roughly 3x narrower, so mean IoU is
0.458. Full numbers and the open options for closing the gap:
modules/peaks/CONCORDANCE.md.
The table above is a record of one run on one machine.
scripts/benchmark_audit.py sweeps a
configuration matrix of input-size tiers by thread counts, and with
--baselines runs the standard pipeline each tool replaces on the same input at
the same core count in the same session:
scripts/benchmark_audit.py --list
scripts/benchmark_audit.py --inputs small --threads 1 2 4 8 --baselines --repeats 3Four choices in it are deliberately unflattering, because a benchmark that only reports favourable numbers is marketing:
- A speedup is printed only when both sides ran in the same session. Comparing a fresh run against a number quoted from a README is how benchmarks start lying.
- Peak RSS comes from the kernel's
ru_maxrssviawait4and never from a sampling thread. A peak is a spike, and a sampler is asleep during it. - For a multi-stage baseline, wall time sums across stages but peak RSS takes
the maximum, since the stages do not overlap. Summing would overstate the
baseline and flatter
pto-core. - Parallel efficiency above 1.0 is flagged rather than celebrated. It almost
always means the 1-thread baseline was contended, and here specifically it
means htslib decompressed BGZF on its own threads: a 1-thread baseline on the
cuttag/profilecase shows 4.32 cores busy, so a figure above 100% is a property of the baseline and not superlinear scaling.
Skipped configurations are listed explicitly, with the note that any speedup claim covers only the rows that actually ran.
One finding still worth acting on: plain gzip caps fastq_stream at roughly
2.2 M reads/s regardless of thread count, and BGZF is 3.2x faster at 12 threads.
That is the framing result from
fastq_stream's architecture doc
observed from the other end.
Findings are logged before they are fixed, and residuals are recorded rather than quietly dropped. Across seven adversarial passes between 2026-08-15 and 2026-09-11 these documents record 74 confirmed defects, 24 of them rated High or Critical, every one carrying a regression test added in the commit that fixed it.
These are living documents. Extend them rather than replacing them when you find or fix something in their scope.
| Document | Scope | Outcome |
|---|---|---|
REVIEW_2026-08-15.md |
cross-module architecture and security | 5 findings, all closed with regression tests |
SECURITY_HTTP_2026-08-15.md |
cuttag_profiler's embedded HTTP server and path confinement |
all 9 closed, one stated residual |
TORTURE_2026-08-17.md |
adversarial fuzzing, sanitizer and concurrency sweep | 5 defects fixed, 1 false positive left unsuppressed |
TORTURE_2026-09-10.md |
fastq_stream, then pto-peaks |
14 and 8 findings respectively |
AUDIT_2026-09-11_fastq_stream.md |
Phred offsets, record framing, per-cycle truncation | 8 findings, all closed |
AUDIT_2026-09-11_genomic_toolkit.md |
sizes, markdup, frip |
9 findings, all closed |
AUDIT_2026-09-11_cuttag_profiler.md |
profile, bin geometry, skipped regions |
6 findings, all closed |
AUDIT_2026-09-11_scrna_matrix.md |
HNSW producers, index concurrency, NumPy boundary | 11 findings, all closed |
The headline findings are worth naming, because in every case the program ran to completion and printed a plausible number:
- A BAM cut at a BGZF block boundary reported half a library at exit 0.
- Regions on a contig absent from the BAM were averaged into the meta-profile as zeros, diluting the curve in direct proportion to how many were skipped.
- An unchecked
n * kin every HNSW producer stored through a null pointer. - Two concurrent index mutations left an index holding half its rows, with no exception raised and a perfectly valid file on disk.
- An iteration cap silently scored bases 4 to 19 orders of magnitude past the significance cutoff as p = 1 for counts above roughly 1e9.
-i X -o Xtruncated the input.- Phred+64 quality strings were read as Phred+33, moving mean Q from 19.85 to 50.85 and the Q30 rate from 0.265 to 1.000.
The recurrence is the methodological finding. Four of the review's five
findings were a previously-closed defect surviving in a sibling function or a
second entry point the earlier pass did not walk. In scrna_matrix, the same
unchecked narrowing and the same unchecked n * k were each found twice. When
you fix one of these, grep for its shape across the tree before calling it done.
Several of the guards here are source-level scans precisely because a rule a
human has to remember is a rule that gets broken in the next sibling function.
pto-peaks has no separate audit document of its own. Its 8 hardening findings
are the second half of the 2026-09-10 pass, and its MACS3 concordance status is
a different question tracked in
modules/peaks/CONCORDANCE.md rather than as a
security finding. modules/scrna_matrix/docs/AUDIT.md is the earlier
module-level equivalent and the model the others follow: reproduction before
fix, honest residuals, and a section for what turned out not to be ours.
The top-level CMakeLists.txt is an aggregate. It adds each module as a
subdirectory and registers every module's tests with one CTest project, so the
whole tree builds and tests in a single pass.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failureRequires CMake 3.20 or newer, a C++20 compiler, and a working OpenMP toolchain
for scrna_matrix.
Configuration fails without it, and this is deliberate. It used to be optional
with a message(WARNING) fallback, and the result was that _OPENMP went
undefined, every #pragma omp compiled to serial code, and the test suite
reported 100% passed while never executing a parallel iteration. A silently
single-threaded engine that CI signs off on is worse than a build that stops.
It is also the reason scrna_matrix ships as its own
wheel rather than inside pto-core.
| Platform | What to do |
|---|---|
| macOS, Apple Clang | brew install libomp, then plain cmake -S . -B build works. The keg is auto-detected. |
| macOS, Homebrew GCC | brew install gcc, then -DCMAKE_C_COMPILER=gcc-15 -DCMAKE_CXX_COMPILER=g++-15. |
| Debian/Ubuntu, Clang | sudo apt-get install libomp-dev |
| Debian/Ubuntu, GCC | Already present as libgomp. |
Apple Clang needs -Xclang -fopenmp and Homebrew installs libomp keg-only,
so CMake's FindOpenMP reports NOT FOUND on a stock Mac even after
brew install libomp. cmake/PtoOpenMP.cmake closes
that gap: it locates the keg and hands FindOpenMP the flags it cannot derive.
For a non-standard Homebrew prefix:
cmake -S . -B build -DLIBOMP_ROOT=$(brew --prefix libomp)That makes the requirement satisfiable. It does not relax it. Detection is
followed by a check_cxx_source_compiles probe that fails the build unless
_OPENMP is actually defined and a #pragma omp parallel region compiles,
because find_package succeeding proves only that the compiler accepts the
flag.
fastq_stream and cuttag_profiler contain no OpenMP code and build without
any of this, under -DPTO_BUILD_SCRNA_MATRIX=OFF.
A host missing one module's dependencies can still build and test the rest.
| Option | Default | Effect |
|---|---|---|
PTO_BUILD_SCRNA_MATRIX |
ON | build modules/scrna_matrix |
PTO_BUILD_FASTQ_STREAM |
ON | build modules/fastq_stream |
PTO_BUILD_CUTTAG_PROFILER |
ON | build modules/cuttag_profiler |
PTO_BUILD_GENOMIC_TOOLKIT |
ON | build modules/genomic_toolkit |
PTO_BUILD_PEAKS |
ON | build modules/peaks |
cmake -S . -B build -DPTO_BUILD_CUTTAG_PROFILER=OFFDisabling every module is a configure-time error rather than an empty build, and CI asserts that it stays one.
| Module | Required | Optional |
|---|---|---|
scrna_matrix |
none (header-only) | pybind11 for Python bindings, OpenMP for parallel k-NN; hnswlib is vendored |
fastq_stream |
zlib; libdeflate (found if installed, otherwise fetched and built statically) |
none |
cuttag_profiler |
htslib via pkg-config |
none |
genomic_toolkit |
none | htslib for BAM/CRAM/.gz input (text paths and the whole test suite work without it), OpenMP for the FRiP reduction |
peaks |
none | libdeflate, else zlib, for BAM/BGZF input. Discovered, never fetched, via PEAKS_WITH_BGZF=AUTO/ON/OFF. Takes no htslib dependency at all. |
On macOS with Homebrew, add -DCMAKE_PREFIX_PATH=/opt/homebrew. httplib and
nlohmann/json are vendored under modules/cuttag_profiler/third_party/.
Without pybind11 the scrna_matrix Python module and its binding tests are
skipped with a configure-time message, and the C++ suite still runs. To include
them, point CMake at an interpreter that has it:
cmake -S . -B build -Dpybind11_DIR=$(python -c 'import pybind11; print(pybind11.get_cmake_dir())')Nothing at the top level is required to use a module, and a module's build may not depend on being invoked from there:
cmake -S modules/fastq_stream -B build/fastq_stream -DCMAKE_BUILD_TYPE=Release
cmake --build build/fastq_stream -jModule-specific options (FQ_*, SCRNA_*, CUTTAG_*, GTK_*, PEAKS_*) are
documented in each module's README and work identically in both modes.
The wheel-facing decisions live in pyproject.toml and
modules/scrna_matrix/pyproject.toml,
each with the reasoning kept inline rather than assumed. The parts worth
knowing without reading both files end to end:
- Two wheels, not one.
pto-coreconfigures withPTO_BUILD_SCRNA_MATRIX=OFF— the split explained above — and its own distribution is ABI-neutral (py3-none-<platform>), because it contains three ordinary executables and a pure-Python wrapper, nothing bound to an interpreter's ABI.scrna-matrixis the opposite: a compiled extension module, tagged normally per interpreter. - htslib is statically linked on Linux, dynamically on macOS, and the
difference is not a style choice. manylinux has no htslib package, so
packaging/scripts/build_htslib_manylinux.shbuilds it from source and links it in statically;ci.yml'sstatic-linkjob asserts the resulting binary carries nolibhts-family shared-object dependency. The same static link succeeds against Homebrew htslib on macOS too, because its remaining need — libcurl — is the system dylib at/usr/lib/libcurl.4.dylib; that is whycuttag_profilerlinks htslib statically there whilefastq_streamandgenomic_toolkitstill link it dynamically, withdelocate-wheelvendoringlibhts,libomp,libdeflateandliblzmaintopto/.dylibs/and rewriting the two binaries' install names. - No
universal2macOS wheel. Homebrew's htslib and libomp are per-architecture; an arm64 runner cannot link an x86_64libhts.dylib, and the cross build either fails at link time or produces a wheeldyldrejects on the user's machine. Both architectures ship as native wheels instead, one runner per architecture (macos-13 for x86_64, macos-14 for arm64). - Every native-ISA flag defaults OFF for a wheel build
(
PROFILER_NATIVE_ARCH,FQ_NATIVE,GTK_NATIVE_ARCH,PEAKS_ENABLE_AVX512). These flags put the build host's instruction set onto the binary; a wheel installs on machines it never saw, and an AVX-512 instruction reaching a CPU without one is aSIGILLwith no diagnostic the user can act on — and not even at import, but partway through someone's batch. - Third-party code is vendored, not fetched at install time.
cpp-httplibandnlohmann/jsonlive undermodules/cuttag_profiler/third_party/;hnswlib, with the local patches recorded inthird_party/hnswlib/VERSION.txt, lives undermodules/scrna_matrix/third_party/. Full license text for all three is inTHIRD_PARTY_LICENSES.md.
pto-core/
├── CMakeLists.txt # aggregate build; owns no sources of its own
├── pyproject.toml # the `pto-core` wheel; see packaging/
├── cmake/ # shared CMake modules (OpenMP toolchain resolution)
├── deploy/ # container entrypoint and demo provisioning
├── docker/ # TSan verification image and suppressions
├── docs/ # repository-wide review and audit documents
├── packaging/ # the wheel's Python source (`pto`) and its own suite
├── scripts/ # verification harnesses
├── tests/validation/ # public-data differential suite; drives the CLIs, links nothing
└── modules/
├── scrna_matrix/ # header-only engine and pybind11 bindings
├── fastq_stream/ # streaming FASTQ QC and trimming
├── cuttag_profiler/ # CUT&Tag profiling with embedded dashboard
├── genomic_toolkit/ # streaming fragment QC
└── peaks/ # pto-peaks: streaming peak caller
CI is not in this directory. GitHub reads workflows only from the repository
root, one level up, and every run step there executes with
working-directory: pto-core. That is load-bearing: the monorepo restructure
moved CMakeLists.txt down a level while the workflow still ran cmake -S .
from the repository root, and all seven jobs failed with an error that reads
like a build failure and was a path failure.
Every directory here holds files. Earlier revisions carried empty include/,
python/, tests/ and third_party/ directories at the root, anticipating a
shared top-level layer: a common headers namespace, a unified Python package,
cross-module integration tests. That layer does not exist and is not planned.
The modules share no code and have disjoint dependency sets, so introducing one
would be a design decision to take deliberately rather than a gap to fill in.
tests/validation/ is not a reinstatement of that layer, and the distinction is
worth being precise about. It contains no C++, links against no module, and no
module's build or test suite references it; nothing under modules/ can break
because of it. It is a Python harness that runs the shipped binaries as black
boxes against public data. The rule it must keep obeying: a module must remain
independently buildable and testable with this directory deleted.
MIT. LICENSE governs the whole of pto-core: the aggregate build
and all five modules, including fastq_stream, which also carries its own
identical copy from before that file existed. First-party sources carry
SPDX-License-Identifier: MIT; that header is a per-file marker rather than a
substitute for the root grant, and no module needs a license file of its own for
the grant to apply.
Vendored third-party code keeps its own license, reproduced in full in
THIRD_PARTY_LICENSES.md: hnswlib (Apache-2.0),
cpp-httplib (MIT), nlohmann/json (MIT).