mirror of
https://github.com/metabarcoding/obitools4.git
synced 2026-03-25 13:30:52 +00:00
Compare commits
3 Commits
push-kzmrq
...
Release_4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c332d3ddc9 | ||
|
|
0580611031 | ||
|
|
c30a22d356 |
16
.github/workflows/obitest.yml
vendored
16
.github/workflows/obitest.yml
vendored
@@ -9,11 +9,11 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '1.23'
|
||||
- name: Checkout obitools4 project
|
||||
uses: actions/checkout@v4
|
||||
- name: Run tests
|
||||
run: make githubtests
|
||||
- name: Checkout obitools4 project
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.23"
|
||||
- name: Run tests
|
||||
run: make githubtests
|
||||
|
||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -62,12 +62,6 @@ jobs:
|
||||
TAG=${GITHUB_REF#refs/tags/Release_}
|
||||
echo "version=$TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install build tools (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install -y musl-tools zlib1g-dev
|
||||
|
||||
- name: Install build tools (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
@@ -80,13 +74,8 @@ jobs:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
VERSION: ${{ steps.get_version.outputs.version }}
|
||||
CC: ${{ matrix.goos == 'linux' && 'musl-gcc' || '' }}
|
||||
run: |
|
||||
if [ "$GOOS" = "linux" ]; then
|
||||
make LDFLAGS='-linkmode=external -extldflags=-static' obitools
|
||||
else
|
||||
make obitools
|
||||
fi
|
||||
make obitools
|
||||
mkdir -p artifacts
|
||||
# Create a single tar.gz with all binaries for this platform
|
||||
tar -czf artifacts/obitools4_${VERSION}_${{ matrix.output_name }}.tar.gz -C build .
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,7 +16,6 @@
|
||||
**/*.tgz
|
||||
**/*.yaml
|
||||
**/*.csv
|
||||
**/*.pb.gz
|
||||
xx
|
||||
|
||||
.rhistory
|
||||
|
||||
127
Makefile
127
Makefile
@@ -2,17 +2,9 @@
|
||||
#export GOBIN=$(GOPATH)/bin
|
||||
#export PATH=$(GOBIN):$(shell echo $${PATH})
|
||||
|
||||
.DEFAULT_GOAL := all
|
||||
|
||||
GREEN := \033[0;32m
|
||||
YELLOW := \033[0;33m
|
||||
BLUE := \033[0;34m
|
||||
NC := \033[0m
|
||||
|
||||
GOFLAGS=
|
||||
LDFLAGS=
|
||||
GOCMD=go
|
||||
GOBUILD=$(GOCMD) build $(GOFLAGS) $(if $(LDFLAGS),-ldflags="$(LDFLAGS)")
|
||||
GOBUILD=$(GOCMD) build $(GOFLAGS)
|
||||
GOGENERATE=$(GOCMD) generate
|
||||
GOCLEAN=$(GOCMD) clean
|
||||
GOTEST=$(GOCMD) test
|
||||
@@ -51,7 +43,7 @@ $(OBITOOLS_PREFIX)$(notdir $(1)): $(BUILD_DIR) $(1) pkg/obioptions/version.go
|
||||
@echo -n - Building obitool $(notdir $(1))...
|
||||
@$(GOBUILD) -o $(BUILD_DIR)/$(OBITOOLS_PREFIX)$(notdir $(1)) ./$(1) \
|
||||
2> $(OBITOOLS_PREFIX)$(notdir $(1)).log \
|
||||
|| { cat $(OBITOOLS_PREFIX)$(notdir $(1)).log; rm -f $(OBITOOLS_PREFIX)$(notdir $(1)).log; exit 1; }
|
||||
|| cat $(OBITOOLS_PREFIX)$(notdir $(1)).log
|
||||
@rm -f $(OBITOOLS_PREFIX)$(notdir $(1)).log
|
||||
@echo Done.
|
||||
endef
|
||||
@@ -68,28 +60,6 @@ endif
|
||||
|
||||
OUTPUT:=$(shell mktemp)
|
||||
|
||||
help:
|
||||
@printf "$(GREEN)OBITools4 Makefile$(NC)\n\n"
|
||||
@printf "$(BLUE)Main targets:$(NC)\n"
|
||||
@printf " %-20s %s\n" "all" "Build all obitools (default)"
|
||||
@printf " %-20s %s\n" "obitools" "Build all obitools binaries to build/"
|
||||
@printf " %-20s %s\n" "test" "Run Go unit tests"
|
||||
@printf " %-20s %s\n" "obitests" "Run integration tests (obitests/)"
|
||||
@printf " %-20s %s\n" "bump-version" "Increment patch version (or set with VERSION=x.y.z)"
|
||||
@printf " %-20s %s\n" "update-deps" "Update all Go dependencies"
|
||||
@printf "\n$(BLUE)Jujutsu workflow:$(NC)\n"
|
||||
@printf " %-20s %s\n" "jjnew" "Document current commit and start a new one"
|
||||
@printf " %-20s %s\n" "jjpush" "Release: describe, bump, generate notes, push PR, tag (VERSION=x.y.z optional)"
|
||||
@printf " %-20s %s\n" "jjfetch" "Fetch latest commits from origin"
|
||||
@printf "\n$(BLUE)Required tools:$(NC)\n"
|
||||
@printf " %-20s " "go"; command -v go >/dev/null 2>&1 && printf "$(GREEN)✓$(NC) %s\n" "$$(go version)" || printf "$(YELLOW)✗ not found$(NC)\n"
|
||||
@printf " %-20s " "git"; command -v git >/dev/null 2>&1 && printf "$(GREEN)✓$(NC) %s\n" "$$(git --version)" || printf "$(YELLOW)✗ not found$(NC)\n"
|
||||
@printf " %-20s " "jj"; command -v jj >/dev/null 2>&1 && printf "$(GREEN)✓$(NC) %s\n" "$$(jj --version)" || printf "$(YELLOW)✗ not found$(NC)\n"
|
||||
@printf " %-20s " "gh"; command -v gh >/dev/null 2>&1 && printf "$(GREEN)✓$(NC) %s\n" "$$(gh --version | head -1)" || printf "$(YELLOW)✗ not found$(NC) (brew install gh)\n"
|
||||
@printf "\n$(BLUE)Optional tools (release notes generation):$(NC)\n"
|
||||
@printf " %-20s " "aichat"; command -v aichat >/dev/null 2>&1 && printf "$(GREEN)✓$(NC) %s\n" "$$(aichat --version)" || printf "$(YELLOW)✗ not found$(NC) (https://github.com/sigoden/aichat)\n"
|
||||
@printf " %-20s " "jq"; command -v jq >/dev/null 2>&1 && printf "$(GREEN)✓$(NC) %s\n" "$$(jq --version)" || printf "$(YELLOW)✗ not found$(NC) (brew install jq)\n"
|
||||
|
||||
all: install-githook obitools
|
||||
|
||||
obitools: $(patsubst %,$(OBITOOLS_PREFIX)%,$(OBITOOLS))
|
||||
@@ -136,20 +106,15 @@ pkg/obioptions/version.go: version.txt .FORCE
|
||||
@rm -f $(OUTPUT)
|
||||
|
||||
bump-version:
|
||||
@echo "Incrementing version..."
|
||||
@current=$$(cat version.txt); \
|
||||
if [ -n "$(VERSION)" ]; then \
|
||||
new_version="$(VERSION)"; \
|
||||
echo "Setting version to $$new_version (was $$current)"; \
|
||||
else \
|
||||
echo "Incrementing version..."; \
|
||||
echo " Current version: $$current"; \
|
||||
major=$$(echo $$current | cut -d. -f1); \
|
||||
minor=$$(echo $$current | cut -d. -f2); \
|
||||
patch=$$(echo $$current | cut -d. -f3); \
|
||||
new_patch=$$((patch + 1)); \
|
||||
new_version="$$major.$$minor.$$new_patch"; \
|
||||
echo " New version: $$new_version"; \
|
||||
fi; \
|
||||
echo " Current version: $$current"; \
|
||||
major=$$(echo $$current | cut -d. -f1); \
|
||||
minor=$$(echo $$current | cut -d. -f2); \
|
||||
patch=$$(echo $$current | cut -d. -f3); \
|
||||
new_patch=$$((patch + 1)); \
|
||||
new_version="$$major.$$minor.$$new_patch"; \
|
||||
echo " New version: $$new_version"; \
|
||||
echo "$$new_version" > version.txt
|
||||
@echo "✓ Version updated in version.txt"
|
||||
@$(MAKE) pkg/obioptions/version.go
|
||||
@@ -165,7 +130,6 @@ jjnew:
|
||||
jjpush:
|
||||
@$(MAKE) jjpush-describe
|
||||
@$(MAKE) jjpush-bump
|
||||
@$(MAKE) jjpush-notes
|
||||
@$(MAKE) jjpush-push
|
||||
@$(MAKE) jjpush-tag
|
||||
@echo "$(GREEN)✓ Release complete$(NC)"
|
||||
@@ -178,62 +142,39 @@ jjpush-bump:
|
||||
@echo "$(BLUE)→ Creating new commit for version bump...$(NC)"
|
||||
@jj new
|
||||
@$(MAKE) bump-version
|
||||
|
||||
jjpush-notes:
|
||||
@version=$$(cat version.txt); \
|
||||
echo "$(BLUE)→ Generating release notes for version $$version...$(NC)"; \
|
||||
release_title="Release $$version"; \
|
||||
release_body=""; \
|
||||
if command -v aichat >/dev/null 2>&1; then \
|
||||
previous_tag=$$(git describe --tags --abbrev=0 --match 'Release_*' 2>/dev/null); \
|
||||
if [ -z "$$previous_tag" ]; then \
|
||||
echo "$(YELLOW)⚠ No previous Release tag found, skipping release notes$(NC)"; \
|
||||
else \
|
||||
raw_output=$$(git log --format="%h %B" "$$previous_tag..HEAD" | \
|
||||
aichat \
|
||||
"Summarize the following commits into a GitHub release note for version $$version. Ignore commits related to version bumps, .gitignore changes, or any internal housekeeping that is irrelevant to end users. Describe each user-facing change precisely without exposing code. Eliminate redundancy. Output strictly valid JSON with no surrounding text, using this exact schema: {\"title\": \"<short release title>\", \"body\": \"<detailed markdown release notes>\"}" 2>/dev/null) || true; \
|
||||
if [ -n "$$raw_output" ]; then \
|
||||
notes=$$(printf '%s\n' "$$raw_output" | python3 tools/json2md.py 2>/dev/null); \
|
||||
if [ -n "$$notes" ]; then \
|
||||
release_title=$$(echo "$$notes" | head -1); \
|
||||
release_body=$$(echo "$$notes" | tail -n +3); \
|
||||
else \
|
||||
echo "$(YELLOW)⚠ JSON parsing failed, using default release message$(NC)"; \
|
||||
fi; \
|
||||
fi; \
|
||||
fi; \
|
||||
fi; \
|
||||
printf '%s' "$$release_title" > /tmp/obitools4-release-title.txt; \
|
||||
printf '%s' "$$release_body" > /tmp/obitools4-release-body.txt; \
|
||||
echo "$(BLUE)→ Setting release notes as commit description...$(NC)"; \
|
||||
jj desc -m "$$release_title"$$'\n\n'"$$release_body"
|
||||
@echo "$(BLUE)→ Documenting version bump commit...$(NC)"
|
||||
@jj auto-describe
|
||||
|
||||
jjpush-push:
|
||||
@echo "$(BLUE)→ Pushing commits...$(NC)"
|
||||
@jj git push --change @
|
||||
@echo "$(BLUE)→ Creating/updating PR...$(NC)"
|
||||
@release_title=$$(cat /tmp/obitools4-release-title.txt 2>/dev/null || echo "Release $$(cat version.txt)"); \
|
||||
release_body=$$(cat /tmp/obitools4-release-body.txt 2>/dev/null || echo ""); \
|
||||
branch=$$(jj log -r @ --no-graph -T 'bookmarks.map(|b| b.name()).join("\n")' 2>/dev/null | head -1); \
|
||||
if [ -n "$$branch" ] && command -v gh >/dev/null 2>&1; then \
|
||||
gh pr create --title "$$release_title" --body "$$release_body" --base master --head "$$branch" 2>/dev/null \
|
||||
|| gh pr edit "$$branch" --title "$$release_title" --body "$$release_body" 2>/dev/null \
|
||||
|| echo "$(YELLOW)⚠ Could not create/update PR$(NC)"; \
|
||||
fi
|
||||
|
||||
jjpush-tag:
|
||||
@version=$$(cat version.txt); \
|
||||
tag_name="Release_$$version"; \
|
||||
release_title=$$(cat /tmp/obitools4-release-title.txt 2>/dev/null || echo "Release $$version"); \
|
||||
release_body=$$(cat /tmp/obitools4-release-body.txt 2>/dev/null || echo ""); \
|
||||
install_section=$$'\n## Installation\n\n### Pre-built binaries\n\nDownload the appropriate archive for your system from the\n[release assets](https://github.com/metabarcoding/obitools4/releases/tag/Release_'"$$version"')\nand extract it:\n\n#### Linux (AMD64)\n```bash\ntar -xzf obitools4_'"$$version"'_linux_amd64.tar.gz\n```\n\n#### Linux (ARM64)\n```bash\ntar -xzf obitools4_'"$$version"'_linux_arm64.tar.gz\n```\n\n#### macOS (Intel)\n```bash\ntar -xzf obitools4_'"$$version"'_darwin_amd64.tar.gz\n```\n\n#### macOS (Apple Silicon)\n```bash\ntar -xzf obitools4_'"$$version"'_darwin_arm64.tar.gz\n```\n\nAll OBITools4 binaries are included in each archive.\n\n### From source\n\nYou can also compile and install OBITools4 directly from source using the\ninstallation script:\n\n```bash\ncurl -L https://raw.githubusercontent.com/metabarcoding/obitools4/master/install_obitools.sh | bash -s -- --version '"$$version"'\n```\n\nBy default binaries are installed in `/usr/local/bin`. Use `--install-dir` to\nchange the destination and `--obitools-prefix` to add a prefix to command names:\n\n```bash\ncurl -L https://raw.githubusercontent.com/metabarcoding/obitools4/master/install_obitools.sh | \\\n bash -s -- --version '"$$version"' --install-dir ~/local --obitools-prefix k\n```\n'; \
|
||||
release_message="$$release_title"$$'\n\n'"$$release_body$$install_section"; \
|
||||
echo "$(BLUE)→ Generating release notes for $$tag_name...$(NC)"; \
|
||||
release_message="Release $$version"; \
|
||||
if command -v orla >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then \
|
||||
previous_patch=$$(( $$(echo $$version | cut -d. -f3) - 1 )); \
|
||||
previous_tag="Release_$$(echo $$version | cut -d. -f1).$$(echo $$version | cut -d. -f2).$$previous_patch"; \
|
||||
raw_output=$$(jj log -r "$$previous_tag::@" -T 'commit_id.short() ++ " " ++ description' | \
|
||||
ORLA_MAX_TOOL_CALLS=50 orla agent -m ollama:qwen3-coder-next:latest \
|
||||
"Summarize the following commits into a GitHub release note for version $$version. Ignore commits related to version bumps, .gitignore changes, or any internal housekeeping that is irrelevant to end users. Describe each user-facing change precisely without exposing code. Eliminate redundancy. Output strictly valid JSON with no surrounding text, using this exact schema: {\"title\": \"<short release title>\", \"body\": \"<detailed markdown release notes>\"}" 2>/dev/null) || true; \
|
||||
if [ -n "$$raw_output" ]; then \
|
||||
sanitized=$$(echo "$$raw_output" | sed -n '/^{/,/^}/p' | tr -d '\000-\011\013-\014\016-\037'); \
|
||||
release_title=$$(echo "$$sanitized" | jq -r '.title // empty' 2>/dev/null) ; \
|
||||
release_body=$$(echo "$$sanitized" | jq -r '.body // empty' 2>/dev/null) ; \
|
||||
if [ -n "$$release_title" ] && [ -n "$$release_body" ]; then \
|
||||
release_message="$$release_title"$$'\n\n'"$$release_body"; \
|
||||
else \
|
||||
echo "$(YELLOW)⚠ JSON parsing failed, using default release message$(NC)"; \
|
||||
fi; \
|
||||
fi; \
|
||||
fi; \
|
||||
echo "$(BLUE)→ Creating tag $$tag_name...$(NC)"; \
|
||||
commit_hash=$$(jj log -r @ --no-graph -T 'commit_id' 2>/dev/null); \
|
||||
git tag -a "$$tag_name" $${commit_hash:+"$$commit_hash"} -m "$$release_message" 2>/dev/null || echo "$(YELLOW)⚠ Tag $$tag_name already exists$(NC)"; \
|
||||
git tag -a "$$tag_name" -m "$$release_message" 2>/dev/null || echo "$(YELLOW)⚠ Tag $$tag_name already exists$(NC)"; \
|
||||
echo "$(BLUE)→ Pushing tag $$tag_name...$(NC)"; \
|
||||
git push origin "$$tag_name" 2>/dev/null || echo "$(YELLOW)⚠ Tag push failed or already pushed$(NC)"; \
|
||||
rm -f /tmp/obitools4-release-title.txt /tmp/obitools4-release-body.txt
|
||||
git push origin "$$tag_name" 2>/dev/null || echo "$(YELLOW)⚠ Tag push failed or already pushed$(NC)"
|
||||
|
||||
jjfetch:
|
||||
@echo "$(YELLOW)→ Pulling latest commits...$(NC)"
|
||||
@@ -241,5 +182,5 @@ jjfetch:
|
||||
@jj new master@origin
|
||||
@echo "$(GREEN)✓ Latest commits pulled$(NC)"
|
||||
|
||||
.PHONY: all obitools update-deps obitests githubtests help jjnew jjpush jjpush-describe jjpush-bump jjpush-notes jjpush-push jjpush-tag jjfetch bump-version .FORCE
|
||||
.PHONY: all obitools update-deps obitests githubtests jjnew jjpush jjpush-describe jjpush-bump jjpush-push jjpush-tag jjfetch bump-version .FORCE
|
||||
.FORCE:
|
||||
|
||||
@@ -32,12 +32,8 @@ The installation script offers several options:
|
||||
>
|
||||
> -p, --obitools-prefix Prefix added to the obitools command names if you
|
||||
> want to have several versions of obitools at the
|
||||
> same time on your system (as example `-p g` will produce
|
||||
> same time on your system (as example `-p g` will produce
|
||||
> `gobigrep` command instead of `obigrep`).
|
||||
>
|
||||
> -j, --jobs Number of parallel jobs used for compilation
|
||||
> (default: 1). Increase this value to speed up
|
||||
> compilation on multi-core systems (e.g., `-j 4`).
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
755
blackboard/Prospective/canonical-super-kmer-strategy.md
Normal file
755
blackboard/Prospective/canonical-super-kmer-strategy.md
Normal file
@@ -0,0 +1,755 @@
|
||||
# Prospective : Index k-mer v3 — Super-kmers canoniques, unitigs, et Aho-Corasick
|
||||
|
||||
## 1. Constat sur l'index v1
|
||||
|
||||
L'index actuel (`.kdi` delta-varint) stocke 18.6 milliards de k-mers (k=31, m=13, P=4096, 2 sets) en 85 Go, soit 4.8-5.6 bytes/k-mer. Les causes :
|
||||
|
||||
- Le canonical standard `min(fwd, rc)` disperse les k-mers sur 62 bits → deltas ~2^40 → 5-6 bytes varint
|
||||
- Les k-mers partagés entre sets sont stockés N fois (une fois par set)
|
||||
- Le matching nécessite N×P ouvertures de fichier (N passes)
|
||||
|
||||
## 2. Observations expérimentales
|
||||
|
||||
### 2.1 Déréplication brute
|
||||
|
||||
Sur un génome de *Betula exilis* 15× couvert, le pipeline `obik lowmask | obik super | obiuniq` réduit **80 Go de fastq.gz en 5.6 Go de fasta.gz** — un facteur 14×. Cela montre que la déréplication au niveau super-kmer est extrêmement efficace et que les super-kmers forment une représentation naturellement compacte.
|
||||
|
||||
### 2.2 Après filtre de fréquence (count > 1)
|
||||
|
||||
En éliminant les super-kmers observés une seule fois (erreurs de séquençage), le fichier passe de 5.6 Go à **2.7 Go de fasta.gz**. Les statistiques détaillées (obicount) :
|
||||
|
||||
| Métrique | Valeur |
|
||||
|----------|--------|
|
||||
| Variants (super-kmers uniques) | 37,294,271 |
|
||||
| Reads (somme des counts) | 148,828,167 |
|
||||
| Symboles (bases totales variants) | 1,415,018,593 |
|
||||
| Longueur moyenne super-kmer | **37.9 bases** |
|
||||
| K-mers/super-kmer moyen (k=31) | **7.9** |
|
||||
| K-mers totaux estimés | **~295M** |
|
||||
| Count moyen par super-kmer | **4.0×** |
|
||||
|
||||
### 2.3 Comparaison avec l'index v1
|
||||
|
||||
| Format | Taille | K-mers | Bytes/k-mer |
|
||||
|--------|--------|--------|-------------|
|
||||
| Index .kdi v1 (set Human dans Contaminent_idx) | 12.8 Go | ~3B | 4.3 |
|
||||
| Delta-varint hypothétique (295M k-mers) | ~1.5 Go | 295M | 5.0 |
|
||||
| Super-kmers 2-bit packed (*Betula* count>1) | ~354 Mo | 295M | **1.2** |
|
||||
| Super-kmers fasta.gz (*Betula* count>1) | 2.7 Go | 295M | 9.2* |
|
||||
|
||||
\* Le fasta.gz inclut les headers, les counts, et la compression gzip — pas directement comparable au format binaire.
|
||||
|
||||
**Le format super-kmer 2-bit est ~4× plus compact que le delta-varint** à nombre égal de k-mers. Cette efficacité vient du fait qu'un super-kmer de 38 bases encode 8 k-mers en ~10 bytes au lieu de 8 × 5 = 40 bytes en delta-varint.
|
||||
|
||||
Note : la comparaison n'est pas directe (Contaminent_idx = génomes assemblés, *Betula* = reads bruts filtrés), mais le ratio bytes/k-mer est comparable car il dépend de la longueur des super-kmers, pas de la source des données.
|
||||
|
||||
## 3. Stratégie proposée : pipeline de construction v3
|
||||
|
||||
### 3.1 Définition du k-mer minimizer-canonique
|
||||
|
||||
On redéfinit la forme canonique d'un k-mer en fonction de son minimiseur :
|
||||
|
||||
```
|
||||
CanonicalKmer(kmer, k, m) :
|
||||
minimizer = plus petit m-mer canonique dans le k-mer
|
||||
si minimizer == forward_mmer(minimizer_pos)
|
||||
→ garder le k-mer tel quel
|
||||
sinon
|
||||
→ prendre le reverse-complement du k-mer
|
||||
```
|
||||
|
||||
Propriétés :
|
||||
- **m impair** → aucun m-mer ne peut être palindromique (`m_mer != RC(m_mer)` toujours) → la canonisation par le minimiseur est toujours non-ambiguë. C'est m, pas k, qui doit être impair : l'ambiguïté viendrait d'un minimiseur palindrome (`min == RC(min)`), auquel cas on ne saurait pas dans quel sens orienter le k-mer/super-kmer.
|
||||
- Tous les k-mers d'un super-kmer partagent le même minimiseur
|
||||
- **La canonisation peut se faire au niveau du super-kmer entier** : si `minimizer != canonical(minimizer)`, on RC le super-kmer complet. Tous les k-mers qu'il contient deviennent automatiquement minimizer-canoniques.
|
||||
|
||||
### 3.2 Pipeline de construction
|
||||
|
||||
```
|
||||
Séquences brutes ([]byte, 1 byte/base)
|
||||
│
|
||||
▼
|
||||
[0] Encodage 2-bit + nettoyage
|
||||
│ - Encoder chaque séquence en 2 bits/base ([]byte packed)
|
||||
│ - Couper aux bases ambiguës (N, R, Y, W, S, K, M, B, D, H, V)
|
||||
│ - Retirer les fragments de longueur < k
|
||||
│ - Résultat : fragments 2-bit clean, prêts pour toutes les opérations
|
||||
▼
|
||||
[1] Filtre de complexité (lowmask sur vecteurs 2-bit)
|
||||
│ Supprime/masque les régions de faible entropie
|
||||
▼
|
||||
[2] Extraction des super-kmers (sur vecteurs 2-bit, non canonisé)
|
||||
│ Chaque super-kmer a un minimiseur et une séquence 2-bit packed
|
||||
▼
|
||||
[3] Canonisation au niveau super-kmer
|
||||
│ Si minimizer != CanonicalKmer(minimizer) → RC le super-kmer (op bit)
|
||||
│ Résultat : super-kmers canoniques 2-bit packed
|
||||
▼
|
||||
[4] Écriture dans les partitions .skm (partition = minimizer % P)
|
||||
│ Format natif 2-bit → écriture directe, pas de conversion
|
||||
▼
|
||||
[5] Déréplication des super-kmers par partition
|
||||
│ Trier les super-kmers (comparaison uint64 sur données packed → très rapide)
|
||||
│ Compter les occurrences identiques
|
||||
│ Résultat : super-kmers uniques avec count
|
||||
▼
|
||||
[6] Construction des unitigs canoniques par partition
|
||||
│ Assembler les super-kmers qui se chevauchent de (k-1) bases
|
||||
│ en chaînes linéaires non-branchantes (tout en 2-bit)
|
||||
│ Propager les counts : vecteur de poids par unitig
|
||||
▼
|
||||
[7] Filtre de fréquence sur le graphe pondéré (voir section 4)
|
||||
│ Supprimer les k-mers (positions) avec poids < seuil
|
||||
│ Re-calculer les unitigs après filtrage
|
||||
▼
|
||||
[8] Stockage des unitigs avec bitmask multiset
|
||||
│ Format compact sur disque (déjà en 2-bit, écriture directe)
|
||||
▼
|
||||
Index v3
|
||||
```
|
||||
|
||||
### 3.2bis Pourquoi encoder en 2-bit dès le début ?
|
||||
|
||||
**Alternative rejetée** : travailler en `[]byte` (1 byte/base) puis encoder en 2-bit seulement pour le stockage final.
|
||||
|
||||
| Aspect | `[]byte` (1 byte/base) | 2-bit packed |
|
||||
|--------|----------------------|--------------|
|
||||
| Programmation | Simple (slicing natif, pas de bit-shift) | Plus complexe (masques, shifts) |
|
||||
| Mémoire par super-kmer (38 bases) | 38 bytes | 10 bytes (**3.8×** moins) |
|
||||
| 37M super-kmers en RAM | ~1.4 Go | ~370 Mo |
|
||||
| Tri (comparaison) | `bytes.Compare` sur slices | Comparaison uint64 (**beaucoup** plus rapide) |
|
||||
| Format .skm | Conversion encode/decode à chaque I/O | Écriture/lecture directe |
|
||||
| RC d'un super-kmer | Boucle sur bytes + lookup | Opérations bit (une instruction pour complement) |
|
||||
|
||||
L'opération la plus coûteuse du pipeline est le **tri des super-kmers** pour la déréplication (étape 5). En 2-bit packed, un super-kmer de ≤32 bases tient dans un `uint64` → tri par comparaison entière (une instruction CPU). Un super-kmer de 33-64 bases tient dans deux `uint64` → tri en deux comparaisons.
|
||||
|
||||
Le code de manipulation 2-bit est plus complexe à écrire mais **s'écrit une seule fois** (bibliothèque de primitives) et bénéficie à toute la chaîne. Le gain en mémoire (4×) et en temps de tri est significatif sur des dizaines de millions de super-kmers.
|
||||
|
||||
### 3.3 Canonisation des super-kmers : pourquoi ça marche
|
||||
|
||||
**Point crucial** : les super-kmers doivent être construits en utilisant le minimiseur **non-canonique** (le m-mer brut tel qu'il apparaît dans la séquence), et non le minimiseur canonique `min(fwd, rc)`.
|
||||
|
||||
**Pourquoi ?** Si on utilise le minimiseur canonique comme critère de regroupement, un même super-kmer pourrait contenir le minimiseur dans ses **deux orientations** à des positions différentes (le m-mer forward à une position, et sa forme RC à une autre position, ayant la même valeur canonique). Dans ce cas, le RC du super-kmer ne résoudrait pas l'ambiguïté.
|
||||
|
||||
**Algorithme correct** :
|
||||
|
||||
1. **Extraction** : construire les super-kmers en regroupant les k-mers consécutifs qui partagent le même m-mer minimal **non-canonique** (le m-mer brut). Au sein d'un tel super-kmer, le minimiseur apparaît toujours dans **une seule orientation**.
|
||||
|
||||
2. **Canonisation** : pour chaque super-kmer, comparer son minimiseur brut à `canonical(minimizer) = min(minimizer, RC(minimizer))` :
|
||||
- Si `minimizer == canonical(minimizer)` → le minimiseur est déjà en forward → garder le super-kmer tel quel
|
||||
- Si `minimizer != canonical(minimizer)` → le minimiseur est en RC → RC le super-kmer entier → le minimiseur apparaît maintenant en forward
|
||||
|
||||
Après cette étape, **chaque k-mer du super-kmer** contient le minimiseur canonique en position forward, ce qui correspond exactement à notre définition de k-mer minimizer-canonique.
|
||||
|
||||
**Note** : cela signifie que l'algorithme `IterSuperKmers` actuel (qui utilise le minimiseur canonique pour le regroupement) doit être modifié pour utiliser le minimiseur brut. C'est un changement dans le critère de rupture des super-kmers : on casse quand le **m-mer minimal brut** change, pas quand le **m-mer minimal canonique** change. Les super-kmers résultants seront potentiellement plus courts (un changement d'orientation du minimiseur force une coupure), mais c'est le prix de la canonicité absolue.
|
||||
|
||||
### 3.4 Déréplication des super-kmers
|
||||
|
||||
Deux super-kmers identiques (même séquence, même minimiseur) correspondent aux mêmes k-mers. On peut les dérépliquer en triant :
|
||||
|
||||
1. Par minimiseur (déjà partitionné)
|
||||
2. Par séquence (tri lexicographique des séquences 2-bit packed)
|
||||
|
||||
Les super-kmers identiques deviennent consécutifs dans le tri → comptage linéaire.
|
||||
|
||||
Le tri peut se faire sur les fichiers .skm d'une partition, en mémoire si la partition tient en RAM, ou par merge-sort externe sinon.
|
||||
|
||||
## 4. Filtre de fréquence
|
||||
|
||||
### 4.1 Problème
|
||||
|
||||
Le filtre de fréquence (`--min-occurrence N`) élimine les k-mers vus moins de N fois. Avec la déréplication des super-kmers, on a un count par super-kmer, pas par k-mer. Un k-mer peut apparaître dans plusieurs super-kmers différents (aux jonctions, ou quand le minimiseur change), donc le count exact d'un k-mer n'est connu qu'après fusion.
|
||||
|
||||
### 4.2 Solution : filtrage sur le graphe de De Bruijn pondéré
|
||||
|
||||
Le filtre de fréquence doit être appliqué **après** la construction des unitigs canoniques (section 5), et non avant. Le pipeline devient :
|
||||
|
||||
```
|
||||
Super-kmers canoniques dérepliqués (avec counts)
|
||||
│
|
||||
▼
|
||||
Construction des unitigs canoniques (section 5)
|
||||
│ Chaque position dans un unitig porte un poids
|
||||
│ = somme des counts des super-kmers couvrant ce k-mer
|
||||
▼
|
||||
Graphe de De Bruijn pondéré (implicite dans les unitigs)
|
||||
│
|
||||
▼
|
||||
Filtrage : supprimer les k-mers (positions) avec poids < seuil
|
||||
│ Cela casse certains unitigs en fragments
|
||||
▼
|
||||
Recalcul des unitigs sur le graphe filtré
|
||||
│
|
||||
▼
|
||||
Unitigs filtrés finaux
|
||||
```
|
||||
|
||||
**Avantages** :
|
||||
- Le filtre opère sur les **k-mers exacts** avec leurs **counts exacts** (pas une approximation par super-kmer)
|
||||
- Le graphe de De Bruijn est implicitement contenu dans les unitigs — pas besoin de le construire explicitement avec une `map[uint64]uint`
|
||||
- Les k-mers aux jonctions de super-kmers ont leurs counts correctement agrégés
|
||||
|
||||
### 4.3 Calcul du poids de chaque position dans un unitig
|
||||
|
||||
Un unitig est construit par chaînage de super-kmers. Chaque super-kmer S de longueur L et count C contribue (L-k+1) k-mers, chacun avec poids C. Quand deux super-kmers se chevauchent de (k-1) bases dans l'unitig, les k-mers de la zone de chevauchement reçoivent la **somme** des counts des deux super-kmers.
|
||||
|
||||
En pratique, lors de la construction de l'unitig par chaînage, on construit un vecteur de poids `weights[0..nkmers-1]` :
|
||||
```
|
||||
Pour chaque super-kmer S (count=C) ajouté à l'unitig:
|
||||
Pour chaque position i couverte par S dans l'unitig:
|
||||
weights[i] += C
|
||||
```
|
||||
|
||||
### 4.4 Filtrage et re-construction
|
||||
|
||||
Après filtrage (`weights[i] < seuil` → supprimer position i), l'unitig est potentiellement coupé en fragments. Chaque fragment continu de positions conservées forme un nouvel unitig (ou super-kmer si court).
|
||||
|
||||
Le recalcul des unitigs après filtrage est trivial : les fragments sont déjà des chemins linéaires, il suffit de vérifier les conditions de non-branchement aux nouvelles extrémités.
|
||||
|
||||
### 4.5 Spectre de fréquence
|
||||
|
||||
Le spectre de fréquence exact peut être calculé directement depuis les vecteurs de poids des unitigs : `weights[i]` donne le count exact du k-mer à la position i. C'est un histogramme sur toutes les positions de tous les unitigs.
|
||||
|
||||
### 4.6 Faisabilité mémoire : graphe pondéré par partition
|
||||
|
||||
Données mesurées sur un index *Betula* (k=31, P=4096 partitions, 1 set, génome assemblé) — distribution des tailles de fichiers .kdi :
|
||||
|
||||
| Métrique | Taille fichier .kdi | K-mers estimés (~5 B/kmer) | Super-kmers (~8 kmer/skm) |
|
||||
|----------|--------------------|-----------------------------|---------------------------|
|
||||
| Mode | 100-200 Ko | 20 000 – 40 000 | 2 500 – 5 000 |
|
||||
| Médiane | ~350-400 Ko | ~70 000 – 80 000 | ~9 000 – 10 000 |
|
||||
| Max | ~2.3 Mo | ~460 000 | ~57 000 |
|
||||
|
||||
Le graphe de De Bruijn pondéré pour une partition nécessite d'extraire tous les k-mers (arêtes) et (k-1)-mers (nœuds) des super-kmers :
|
||||
|
||||
| Partition | K-mers (arêtes) | RAM arêtes (~20 B) | RAM nœuds (~16 B) | Total |
|
||||
|-----------|-----------------|--------------------|--------------------|-------|
|
||||
| Typique (~10K skm, 38 bases avg) | ~80K | ~1.6 Mo | ~1.3 Mo | **~3 Mo** |
|
||||
| Maximale (~57K skm) | ~460K | ~9.2 Mo | ~7.4 Mo | **~17 Mo** |
|
||||
|
||||
C'est **largement en mémoire**. Les partitions étant indépendantes, elles peuvent être traitées en parallèle par un pool de goroutines. Avec 8 goroutines : **~136 Mo** au pic — négligeable. Les tableaux sont réutilisables entre partitions (allocation unique).
|
||||
|
||||
**Conclusion** : la construction du graphe de De Bruijn pondéré partition par partition est non seulement faisable mais triviale en termes de mémoire. C'est un argument fort en faveur de l'approche « filtre après unitigs » plutôt que « filtre sur super-kmers ».
|
||||
|
||||
### 4.7 Invariance de la distribution par rapport à la canonisation
|
||||
|
||||
La redéfinition du k-mer canonique (par le minimiseur au lieu de `min(fwd, rc)`) ne change **rien** à l'ensemble des k-mers ni à leur répartition par partition :
|
||||
|
||||
- C'est une **bijection** : chaque k-mer a toujours exactement un représentant canonique, on change juste lequel des deux brins on choisit
|
||||
- Le partitionnement se fait sur `canonical(minimizer) % P` — la valeur du minimiseur canonique est la même dans les deux conventions
|
||||
- **Même nombre de k-mers par partition, même distribution de tailles**
|
||||
- **Même topologie du graphe de De Bruijn** (mêmes nœuds, mêmes arêtes)
|
||||
|
||||
Ce qui change, c'est **l'orientation** : avec la canonicité par minimiseur, les unitigs canoniques ne suivent que les arêtes « forward » (`suffix(S1) == prefix(S2)`, identité exacte). Certaines arêtes traversables en RC dans BCALM2 deviennent des points de cassure. Le graphe n'est pas plus gros — il suffit de ne construire que des unitigs canoniques, ce qui **simplifie** l'algorithme (pas de gestion des traversées de brin).
|
||||
|
||||
## 5. Construction des unitigs canoniques
|
||||
|
||||
### 5.1 Définition : unitig canonique absolu
|
||||
|
||||
Un **unitig canonique** est un chemin linéaire non-branchant dans le graphe de De Bruijn où :
|
||||
1. Chaque k-mer est **minimizer-canonique** (le minimiseur y apparaît en forward)
|
||||
2. Chaque super-kmer constituant est **canonique** (même convention)
|
||||
3. Le chaînage se fait **sans traversée de brin** : `suffix(k-1, S1) == prefix(k-1, S2)` dans le même sens (pas en RC)
|
||||
|
||||
C'est plus restrictif que les unitigs BCALM2 (qui autorisent `suffix(S1) == RC(prefix(S2))`), mais cela garantit que **tout k-mer extrait par fenêtre glissante est directement dans sa forme canonique**, sans re-canonisation.
|
||||
|
||||
### 5.2 Pourquoi la canonicité absolue est essentielle
|
||||
|
||||
**Matching** : les k-mers requête sont canonisés une fois (par le minimiseur), puis comparés directement aux k-mers de l'unitig par scan. Pas de re-canonisation à la volée → plus rapide, plus simple.
|
||||
|
||||
**Opérations ensemblistes** : deux index utilisant la même convention produisent les mêmes unitigs canoniques pour les mêmes k-mers. L'intersect/union peut opérer par comparaison directe de séquences triées.
|
||||
|
||||
**Bitmask multiset** : la fusion de N sets est triviale — merger des listes de super-kmers/unitigs canoniques triés par séquence.
|
||||
|
||||
**Déterminisme** : un ensemble de k-mers produit toujours les mêmes unitigs canoniques, quel que soit l'ordre d'insertion ou la source des données.
|
||||
|
||||
### 5.3 Impact sur la compaction
|
||||
|
||||
La contrainte canonique interdit les traversées de brin aux jonctions → les unitigs canoniques sont **plus courts** que les unitigs BCALM2. Estimation :
|
||||
- BCALM2 (unitigs libres) : 63 bases moyennes (mesuré sur *Betula*)
|
||||
- Unitigs canoniques : probablement ~45-55 bases moyennes
|
||||
- Super-kmers dérepliqués : 38 bases moyennes
|
||||
|
||||
Le facteur de compaction est légèrement réduit mais le gain en simplicité opérationnelle compense largement.
|
||||
|
||||
### 5.4 Construction par partition — le vrai graphe de De Bruijn
|
||||
|
||||
Les super-kmers canoniques dérepliqués sont des **chemins** dans le graphe de De Bruijn, pas des nœuds. On ne peut pas les chaîner directement comme des nœuds car :
|
||||
- Deux super-kmers peuvent **se chevaucher** (partager des k-mers aux jonctions)
|
||||
- Un super-kmer court peut avoir ses k-mers **inclus** dans un super-kmer plus long
|
||||
|
||||
Un super-kmer de longueur L contient (L-k+1) k-mers, soit (L-k+1) arêtes et (L-k+2) nœuds ((k-1)-mers) dans le graphe de De Bruijn.
|
||||
|
||||
#### 5.4.1 Nœuds = (k-1)-mers, Arêtes = k-mers
|
||||
|
||||
Le graphe de De Bruijn par partition a :
|
||||
- **Nœuds** : les (k-1)-mers uniques (extraits de toutes les positions dans les super-kmers)
|
||||
- **Arêtes** : les k-mers (chaque position dans un super-kmer = une arête entre deux (k-1)-mers consécutifs)
|
||||
- **Poids** : chaque arête (k-mer) porte le count du super-kmer qui la contient
|
||||
|
||||
Les branchements (nœud avec degré entrant > 1 ou degré sortant > 1) peuvent être :
|
||||
- Aux **bords** des super-kmers (jonctions entre super-kmers)
|
||||
- Aux **positions internes** si un k-mer d'un autre super-kmer rejoint un (k-1)-mer interne
|
||||
|
||||
#### 5.4.2 Graphe complet nécessaire
|
||||
|
||||
Construire le graphe avec **tous** les (k-1)-mers (internes et bords) est nécessaire pour détecter correctement les branchements. Se limiter aux seuls bords de super-kmers serait incorrect car un (k-1)-mer de bord d'un super-kmer peut correspondre à un nœud interne d'un autre super-kmer.
|
||||
|
||||
Pour une partition typique de 10K super-kmers de longueur moyenne 38 bases → ~80K k-mers → ~80K arêtes et ~80K nœuds. Voir section 4.6 pour la faisabilité mémoire (~3 Mo par partition typique, ~17 Mo max).
|
||||
|
||||
#### 5.4.3 Structure de données : tableau trié d'arêtes
|
||||
|
||||
Plutôt que des hash maps, on utilise un **tableau trié** pour le graphe :
|
||||
|
||||
```go
|
||||
type Edge struct {
|
||||
srcKmer uint64 // (k-1)-mer source (prefix du k-mer)
|
||||
dstKmer uint64 // (k-1)-mer destination (suffix du k-mer)
|
||||
weight int32 // count du super-kmer contenant ce k-mer
|
||||
}
|
||||
```
|
||||
|
||||
Pour chaque super-kmer S de longueur L et count C, on émet (L-k+1) arêtes. Tableau total pour une partition typique : ~80K × 20 bytes = **~1.6 Mo**.
|
||||
|
||||
On trie par `srcKmer` pour obtenir la liste d'adjacence sortante, ou on construit deux vues triées (par src et par dst) pour avoir adjacence entrante et sortante.
|
||||
|
||||
#### 5.4.4 Détection des unitigs canoniques
|
||||
|
||||
Un unitig canonique est un chemin maximal non-branchant. L'algorithme :
|
||||
|
||||
```
|
||||
1. Extraire toutes les arêtes des super-kmers → tableau edges[]
|
||||
2. Trier edges[] par srcKmer → vue sortante
|
||||
Trier une copie par dstKmer → vue entrante
|
||||
3. Pour chaque (k-1)-mer unique :
|
||||
- degré_sortant = nombre d'arêtes avec ce srcKmer
|
||||
- degré_entrant = nombre d'arêtes avec ce dstKmer
|
||||
- Si degré_sortant == 1 ET degré_entrant == 1 → nœud interne d'unitig
|
||||
- Sinon → nœud de branchement (début ou fin d'unitig)
|
||||
4. Parcourir les chemins non-branchants pour construire les unitigs
|
||||
- Chaque unitig est une séquence de (k-1)-mers chaînés
|
||||
- Le vecteur de poids est la séquence des weight des arêtes traversées
|
||||
```
|
||||
|
||||
Les (k-1)-mers ne sont **pas canonisés** — on respecte l'orientation des super-kmers canoniques. Le chaînage est strictement orienté.
|
||||
|
||||
#### 5.4.5 Estimation mémoire
|
||||
|
||||
| Partition | K-mers (arêtes) | RAM arêtes | RAM nœuds | Total |
|
||||
|-----------|-----------------|------------|-----------|-------|
|
||||
| Typique (~10K skm, 38 bases avg) | ~80K | ~1.6 Mo | ~1.3 Mo | **~3 Mo** |
|
||||
| Maximale (~57K skm) | ~460K | ~9.2 Mo | ~7.4 Mo | **~17 Mo** |
|
||||
|
||||
Avec traitement parallèle par un pool de G goroutines : RAM max = G × 17 Mo. Avec G=8 : **~136 Mo** au pic. Le tableau d'arêtes est réutilisable entre partitions (allocation unique, remise à zéro).
|
||||
|
||||
Complexité : O(E log E) par partition, avec E = nombre total de k-mers. Dominé par les deux tris.
|
||||
|
||||
### 5.5 Graphe par minimiseur, pas par partition
|
||||
|
||||
Les k-mers (arêtes) sont partitionnés par minimiseur. Deux k-mers adjacents dans le graphe de De Bruijn peuvent avoir des minimiseurs différents — c'est exactement ce qui définit les frontières de super-kmers. Si on construit le graphe par partition (qui regroupe plusieurs minimiseurs), des (k-1)-mers de jonction entre minimiseurs différents apparaîtraient comme nœuds partagés entre partitions → le graphe par partition n'est pas autonome.
|
||||
|
||||
**Solution : construire un graphe par minimiseur.**
|
||||
|
||||
Un super-kmer est par définition un chemin dont **tous les k-mers partagent le même minimiseur**. Donc :
|
||||
- Toutes les arêtes d'un super-kmer appartiennent à un seul minimiseur
|
||||
- Chaque graphe par minimiseur est **100% autonome** : toutes ses arêtes et nœuds internes sont auto-contenus
|
||||
- Les (k-1)-mers aux bords des super-kmers qui touchent un autre minimiseur sont des extrémités (degré 0 dans ce graphe) → bouts d'unitig naturels
|
||||
- Aucune jonction inter-graphe → pas de cassure artificielle d'unitig
|
||||
|
||||
**Taille des graphes** : avec ~16K minimiseurs théoriques par partition (P=4096, m=13), le calcul naïf donne ~5 arêtes/minimiseur. Mais en pratique, beaucoup de minimiseurs ne sont pas représentés (séquences biologiques, pas aléatoires) et la distribution est très inégale. Si seuls ~500-1000 minimiseurs sont effectivement présents dans une partition typique de 80K arêtes, on a plutôt **80-160 arêtes en moyenne** par minimiseur, avec une queue de distribution vers les centaines ou milliers pour les minimiseurs les plus fréquents. Même dans ce cas, les graphes restent petits (quelques Ko à quelques dizaines de Ko).
|
||||
|
||||
*À mesurer* : nombre de minimiseurs distincts par partition et distribution du nombre de k-mers par minimiseur sur un index existant.
|
||||
|
||||
**Algorithme** : les super-kmers étant déjà triés par minimiseur dans la partition, on itère séquentiellement et on construit/détruit un petit graphe à chaque changement de minimiseur. C'est plus simple que le graphe par partition — pas de tri global de toutes les arêtes, juste un buffer local réutilisé.
|
||||
|
||||
**Les unitigs résultants** sont les chemins maximaux non-branchants au sein d'un minimiseur. Un unitig ne traverse jamais une frontière de minimiseur, ce qui est correct : tous les k-mers d'un unitig partagent le même minimiseur canonique, ce qui renforce la propriété de canonicité absolue.
|
||||
|
||||
### 5.6 Quand les unitigs canoniques n'aident pas
|
||||
|
||||
- Si les super-kmers sont courts (peu de chevauchement entre super-kmers adjacents)
|
||||
- Si le graphe est très branché (zones de divergence entre génomes)
|
||||
- Si beaucoup de jonctions se font par traversée de brin (la contrainte canonique empêche la fusion)
|
||||
- Données metabarcoding avec grande diversité taxonomique → courts unitigs
|
||||
|
||||
Dans ces cas, stocker les super-kmers dérepliqués directement est suffisant — ils sont déjà canoniques par construction.
|
||||
|
||||
## 6. Construction multiset : super-kmers par set, graphe commun
|
||||
|
||||
### 6.1 Pipeline en deux phases
|
||||
|
||||
La construction d'un index multiset (N sets) se fait en deux phases distinctes :
|
||||
|
||||
**Phase 1 — Par set (indépendant, parallélisable)** :
|
||||
|
||||
Chaque set i (i = 0..N-1) produit indépendamment ses super-kmers canoniques :
|
||||
```
|
||||
Set i : séquences → [0] 2-bit → [1] lowmask → [2] super-kmers → [3] canonisation → [4] partition .skm_i
|
||||
```
|
||||
Puis déréplication par partition : super-kmers triés avec counts, écrits dans des fichiers `.skm` distincts par set.
|
||||
|
||||
**Phase 2 — Par partition, tous sets confondus** :
|
||||
|
||||
Pour chaque partition P (parallélisable par goroutine) :
|
||||
```
|
||||
.skm_0[P], .skm_1[P], ..., .skm_{N-1}[P] (super-kmers triés de chaque set)
|
||||
│
|
||||
▼
|
||||
[a] N-way merge des super-kmers triés
|
||||
│ Même super-kmer dans sets i et j → fusionner en vecteur de counts [c_0, ..., c_{N-1}]
|
||||
│ Super-kmer uniquement dans set i → counts = [0, ..., c_i, ..., 0]
|
||||
▼
|
||||
[b] Extraction des arêtes du graphe de De Bruijn
|
||||
│ Chaque k-mer (arête) porte un vecteur de poids [w_0, ..., w_{N-1}]
|
||||
│ w_i = count du super-kmer contenant ce k-mer dans le set i
|
||||
▼
|
||||
[c] Construction d'un SEUL graphe de De Bruijn par partition
|
||||
│ Les branchements sont définis par l'UNION de tous les sets :
|
||||
│ si un (k-1)-mer a degré > 1 dans n'importe quel set, c'est un branchement
|
||||
▼
|
||||
[d] Extraction des unitigs canoniques communs
|
||||
│ Même séquence pour tous les sets
|
||||
│ Chaque position porte un vecteur de poids (un count par set)
|
||||
▼
|
||||
[e] Filtre de fréquence (optionnel, par set ou global)
|
||||
▼
|
||||
[f] Encodage du bitmask par runs le long de chaque unitig
|
||||
▼
|
||||
Écriture dans le .sku de la partition
|
||||
```
|
||||
|
||||
### 6.2 Le graphe est défini par l'union
|
||||
|
||||
Point crucial : les unitigs sont déterminés par la **topologie de l'union** de tous les sets. Un branchement dans un seul set force une coupure d'unitig pour tous les sets. Cela garantit que :
|
||||
- Les unitigs sont les mêmes quelle que soit l'ordre des sets
|
||||
- Un k-mer donné se trouve toujours au même endroit (même unitig, même position)
|
||||
- Les opérations ensemblistes (intersect, union, difference) opèrent sur les mêmes unitigs
|
||||
|
||||
### 6.3 Arêtes à vecteur de poids
|
||||
|
||||
La structure Edge (section 5.4.3) est étendue pour le multiset :
|
||||
|
||||
```go
|
||||
type Edge struct {
|
||||
srcKmer uint64 // (k-1)-mer source
|
||||
dstKmer uint64 // (k-1)-mer destination
|
||||
weights []int32 // weights[i] = count dans le set i (0 si absent)
|
||||
}
|
||||
```
|
||||
|
||||
Pour la détection des branchements, le degré d'un nœud est le nombre d'arêtes distinctes (par dstKmer pour le degré sortant), **indépendamment** des sets. Une arête présente dans le set 0 mais pas le set 1 compte quand même.
|
||||
|
||||
### 6.4 Bitmask par runs le long des unitigs
|
||||
|
||||
Le long d'un unitig, le bitmask (quels sets contiennent ce k-mer) change rarement — les régions conservées entre génomes sont longues. On encode :
|
||||
|
||||
```
|
||||
unitig_bitmask = [(bitmask_1, run_length_1), (bitmask_2, run_length_2), ...]
|
||||
```
|
||||
|
||||
Où `bitmask_i` a un bit par set (bit j = 1 si `weights[j] > 0` à cette position).
|
||||
|
||||
Pour un unitig de 70 k-mers avec 2 sets :
|
||||
- Si complètement partagé : 1 run `(0b11, 70)` → 2 bytes
|
||||
- Si divergent au milieu : 2-3 runs → 4-6 bytes
|
||||
- Pire cas : 70 runs → 140 bytes (très rare)
|
||||
|
||||
### 6.5 Impact mémoire du multiset
|
||||
|
||||
Le vecteur de poids par arête augmente la taille du graphe :
|
||||
- 1 set : `weight int32` → 4 bytes/arête
|
||||
- N sets : `weights [N]int32` → 4N bytes/arête
|
||||
|
||||
Pour la partition typique (~80K arêtes) avec N=2 sets : overhead = 80K × 4 = **320 Ko** supplémentaires. Négligeable.
|
||||
|
||||
Pour N=64 sets (cas extrême) : 80K × 256 = **~20 Mo** par partition. Reste faisable mais les sets très nombreux pourraient nécessiter un encodage plus compact (sparse vector si beaucoup de zéros).
|
||||
|
||||
### 6.6 Merge des super-kmers : N-way sur séquences triées
|
||||
|
||||
Le merge des N listes de super-kmers triés (par séquence 2-bit) est un N-way merge classique avec min-heap :
|
||||
- Chaque .skm est déjà trié par séquence (étape de déréplication)
|
||||
- On compare les séquences 2-bit packed (comparaison uint64, très rapide)
|
||||
- Quand le même super-kmer apparaît dans plusieurs sets, on fusionne les counts
|
||||
- Quand un super-kmer est unique à un set, les autres counts sont 0
|
||||
|
||||
C'est analogue au `KWayMerge` existant sur les k-mers triés, étendu aux super-kmers.
|
||||
|
||||
## 7. Format de stockage v3 : fichiers parallèles
|
||||
|
||||
### 7.1 Architecture : 3 fichiers par partition
|
||||
|
||||
Pour chaque partition, trois fichiers alignés :
|
||||
|
||||
```
|
||||
index_v3/
|
||||
metadata.toml
|
||||
parts/
|
||||
part_PPPP.sku # séquences 2-bit des unitigs concaténés
|
||||
part_PPPP.skx # index par minimiseur (offsets dans .sku)
|
||||
part_PPPP.skb # bitmask multiset (1 entrée par k-mer)
|
||||
...
|
||||
set_N/spectrum.bin # spectre de fréquence par set
|
||||
```
|
||||
|
||||
### 7.2 Fichier .sku — séquences d'unitigs concaténées
|
||||
|
||||
Tous les unitigs d'une partition sont concaténés bout à bout en 2-bit packed, **ordonnés par minimiseur**. Entre deux unitigs, pas de séparateur dans le flux 2-bit.
|
||||
|
||||
Un **tableau de longueurs** stocké en en-tête ou dans le .skx donne la longueur (en bases) de chaque unitig dans l'ordre. Ce tableau permet :
|
||||
- De retrouver les frontières d'unitigs
|
||||
- De savoir si un match AC chevauche une jonction (à filtrer)
|
||||
- D'indexer directement un unitig par son numéro
|
||||
|
||||
```
|
||||
Format .sku :
|
||||
Magic: "SKU\x01" (4 bytes)
|
||||
TotalBases: uint64 LE (nombre total de bases dans la partition)
|
||||
NUnitigs: uint64 LE (nombre d'unitigs)
|
||||
Lengths: [NUnitigs]varint (longueur en bases de chaque unitig)
|
||||
Sequence: ceil(TotalBases/4) bytes (flux 2-bit continu)
|
||||
```
|
||||
|
||||
### 7.3 Fichier .skx — index par minimiseur
|
||||
|
||||
Pour chaque minimiseur présent dans la partition, l'index donne l'offset (en bases) dans le flux .sku et le nombre d'unitigs :
|
||||
|
||||
```
|
||||
Format .skx :
|
||||
Magic: "SKX\x01" (4 bytes)
|
||||
NMinimizers: uint32 LE (nombre de minimiseurs présents)
|
||||
Entries: [NMinimizers] {
|
||||
Minimizer: uint64 LE (valeur du minimiseur canonique)
|
||||
BaseOffset: uint64 LE (offset en bases dans le flux .sku)
|
||||
UnitigOffset: uint32 LE (index du premier unitig de ce minimiseur dans le tableau de longueurs)
|
||||
NUnitigs: uint32 LE (nombre d'unitigs pour ce minimiseur)
|
||||
}
|
||||
```
|
||||
|
||||
Les entrées sont triées par `Minimizer` → recherche binaire en O(log N).
|
||||
|
||||
Pour accéder aux unitigs d'un minimiseur donné :
|
||||
1. Recherche binaire dans le .skx → `BaseOffset`, `UnitigOffset`, `NUnitigs`
|
||||
2. Seek dans le .sku au bit `BaseOffset × 2`
|
||||
3. Lecture de `NUnitigs` unitigs (longueurs dans le tableau à partir de `UnitigOffset`)
|
||||
|
||||
### 7.4 Fichier .skb — bitmask multiset parallèle
|
||||
|
||||
Le fichier bitmask est **aligné position par position** avec le flux de k-mers des unitigs. Chaque k-mer (position dans un unitig) a exactement une entrée dans le .skb, dans le même ordre que les k-mers apparaissent en lisant les unitigs séquentiellement.
|
||||
|
||||
```
|
||||
Format .skb :
|
||||
Magic: "SKB\x01" (4 bytes)
|
||||
TotalKmers: uint64 LE (nombre total de k-mers)
|
||||
NSets: uint8 (nombre de sets)
|
||||
BitmaskSize: uint8 (ceil(NSets/8) bytes par entrée)
|
||||
Bitmasks: [TotalKmers × BitmaskSize] bytes
|
||||
```
|
||||
|
||||
**Accès direct** : la position absolue d'un k-mer dans le flux d'unitigs (offset en k-mers depuis le début de la partition) donne directement l'index dans le fichier .skb :
|
||||
```
|
||||
bitmask_offset = header_size + kmer_position × BitmaskSize
|
||||
```
|
||||
|
||||
Pour 2 sets : 1 byte par k-mer (6 bits inutilisés).
|
||||
Pour ≤8 sets : 1 byte par k-mer.
|
||||
Pour ≤16 sets : 2 bytes par k-mer.
|
||||
|
||||
**Coût** : pour 295M k-mers (*Betula*, 2 sets) : 295 Mo. Pour l'index Contaminent_idx (18.6B k-mers, 2 sets) : ~18.6 Go. C'est significatif — voir section 7.5 pour la compression.
|
||||
|
||||
### 7.5 Compression du bitmask : RLE ou non ?
|
||||
|
||||
| Approche | Taille (2 sets, 295M k-mers) | Accès |
|
||||
|----------|------------------------------|-------|
|
||||
| Non compressé (1 byte/k-mer) | 295 Mo | O(1) direct |
|
||||
| RLE par unitig | ~10-50 Mo (estimé) | O(decode) par unitig |
|
||||
| Bitset par set (1 bit/k-mer/set) | 74 Mo | O(1) direct |
|
||||
|
||||
L'approche **bitset par set** (1 bit par k-mer par set, packed en bytes) est un bon compromis :
|
||||
- 2 sets : 2 bits/k-mer → ~74 Mo (vs 295 Mo non compressé)
|
||||
- Accès O(1) : `bit = (data[kmer_pos / 4] >> ((kmer_pos % 4) × 2)) & 0x3`
|
||||
- Pas besoin de décompression séquentielle
|
||||
|
||||
Pour les très grands index (18.6B k-mers), même le bitset fait ~4.6 Go. Le RLE par minimiseur (ou par unitig) pourrait réduire à ~1-2 Go mais perd l'accès O(1).
|
||||
|
||||
**Recommandation** : bitset packed pour ≤8 sets (accès O(1)), RLE pour >8 sets ou très grands index.
|
||||
|
||||
## 8. Matching avec Aho-Corasick sur le flux d'unitigs
|
||||
|
||||
### 8.1 Principe
|
||||
|
||||
Pour chaque partition dont les k-mers requête partagent le minimiseur :
|
||||
1. Seek dans le .sku au bloc du minimiseur (via .skx)
|
||||
2. Construire un automate AC avec les k-mers requête canoniques de ce minimiseur
|
||||
3. Scanner le flux 2-bit des unitigs de ce minimiseur
|
||||
4. Pour chaque match : vérifier qu'il ne chevauche pas une frontière d'unitig
|
||||
5. Pour chaque match valide : lookup dans le .skb à la position correspondante → bitmask
|
||||
|
||||
### 8.2 Le problème des faux matches aux jonctions
|
||||
|
||||
En 2-bit, pas de 5e lettre pour séparer les unitigs. Le scan AC sur le flux continu peut produire des matches à cheval sur deux unitigs adjacents.
|
||||
|
||||
**Solution : post-filtrage par le tableau de longueurs.**
|
||||
|
||||
Pendant le scan, on maintient un compteur de position et un index dans le tableau de longueurs (préfixe cumulé). Quand un match est trouvé à la position `p` :
|
||||
- Le match couvre les bases `[p, p+k-1]`
|
||||
- Si ces bases chevauchent une frontière d'unitig → faux positif, ignorer
|
||||
- Sinon → match valide
|
||||
|
||||
Le coût du post-filtrage est O(1) par match (le compteur de frontière avance séquentiellement).
|
||||
|
||||
**Estimation du taux de faux positifs** : avec des unitigs de ~50 bases en moyenne, une jonction tous les ~50 bases, et k=31 : ~31/50 = ~62% des positions de jonction peuvent produire un faux match. Mais seule une infime fraction de ces positions correspond à un pattern dans l'automate AC. En pratique, le nombre de faux positifs est négligeable.
|
||||
|
||||
### 8.3 Du match à la position absolue dans le .skb
|
||||
|
||||
Un match AC à la position `p` dans le flux du minimiseur se traduit en position k-mer dans le .skb :
|
||||
|
||||
```
|
||||
kmer_position_in_partition = base_offset_of_minimizer_in_partition
|
||||
+ p
|
||||
- (nombre de bases de padding/frontières avant p)
|
||||
```
|
||||
|
||||
En fait, si le tableau de longueurs donne les longueurs d'unitigs en bases, la position k-mer cumulative est :
|
||||
```
|
||||
Pour l'unitig i contenant le match :
|
||||
kmer_base = somme des longueurs des unitigs 0..i-1
|
||||
kmer_offset_in_unitig = p - kmer_base
|
||||
kmer_index = somme des (len_j - k + 1) pour j=0..i-1 + kmer_offset_in_unitig
|
||||
```
|
||||
|
||||
Ce `kmer_index` est l'index direct dans le fichier .skb.
|
||||
|
||||
### 8.4 Comparaison avec le merge-scan v1
|
||||
|
||||
| Aspect | Merge-scan (v1) | AC sur unitigs (v3) |
|
||||
|--------|----------------|---------------------|
|
||||
| Pré-requis | Tri des requêtes O(Q log Q) | Construction automate AC O(Q×k) |
|
||||
| Seek | .kdx sparse index | .skx index par minimiseur |
|
||||
| Scan | O(Q + K) merge linéaire par set | O(bases_du_minimiseur + matches) |
|
||||
| Multi-set | **N passes** (une par set) | **1 seule passe** (bitmask .skb) |
|
||||
| I/O | N×P ouvertures de fichier | 1 seek + lecture séquentielle + lookup .skb |
|
||||
| Accès bitmask | implicite (chaque .kdi = 1 set) | O(1) dans .skb |
|
||||
|
||||
Le gain principal du v3 est l'**élimination des N passes** : au lieu de scanner N fois (une par set), on scanne une seule fois et on consulte le bitmask. Pour N=2 sets et P=4096 partitions, cela réduit les ouvertures de fichier de 2×4096 = 8192 à 4096.
|
||||
|
||||
## 9. Estimations de taille et validation expérimentale
|
||||
|
||||
### 9.1 Cas mesuré : *Betula exilis* 15× (reads bruts, count > 1)
|
||||
|
||||
| Métrique | Valeur |
|
||||
|----------|--------|
|
||||
| Super-kmers uniques (count > 1) | 37.3M |
|
||||
| Longueur moyenne | 37.9 bases |
|
||||
| Bases totales | 1.415G |
|
||||
|
||||
**Stockage binaire 2-bit packed** :
|
||||
- Séquences : 1.415G / 4 = **354 Mo**
|
||||
- Headers (longueur varint + minimiseur) : 37.3M × ~4 bytes = **150 Mo**
|
||||
- Bitmask (1 set → 0 bytes, ou 2 sets → 1 byte/entrée = 37 Mo)
|
||||
- **Total estimé : ~500-550 Mo** pour un set
|
||||
|
||||
### 9.2 Extrapolation pour l'index Plants+Human (2 sets)
|
||||
|
||||
L'index v1 actuel contient 18.6B k-mers en 85 Go. Avec le pipeline v3 :
|
||||
|
||||
**Scénario reads bruts 15× par génome** (extrapolé depuis *Betula exilis*) :
|
||||
- *Betula exilis* mesuré : ~37M super-kmers, ~1.4G bases → ~500 Mo
|
||||
- Proportionnellement pour l'index Contaminent_idx (18.6B k-mers) : **~2-5 Go**
|
||||
|
||||
**Scénario génome assemblé (pas de filtre de fréquence)** :
|
||||
- Un génome assemblé de 3 Gbases → estimation ~80M super-kmers × 38 bases → **760 Mo**
|
||||
- Un génome assemblé de 10 Gbases → estimation ~350M super-kmers × 38 bases → **3.3 Go**
|
||||
- Avec overlap multiset : super-kmers partagés fusionnés (bitmask) → **~4 Go**
|
||||
|
||||
**Le gain est spectaculaire dans les deux scénarios** :
|
||||
- Reads bruts : facteur **~30-40×** grâce à la déréplication + filtre de fréquence
|
||||
- Génomes assemblés : facteur **~20×** grâce au format super-kmer seul
|
||||
|
||||
Le format super-kmer est intrinsèquement plus efficace que le delta-varint car il exploite la structure locale du graphe de De Bruijn : des k-mers consécutifs partagent (k-1) bases, encodées une seule fois dans le super-kmer.
|
||||
|
||||
### 9.3 Validation expérimentale : unitigs BCALM2
|
||||
|
||||
*Betula exilis* 15×, après lowmask + super-kmers canoniques + déréplication + filtre count>1, passé dans BCALM2 (`-kmer-size 31 -abundance-min 1`) :
|
||||
|
||||
| Métrique | Super-kmers (count>1) | Unitigs (BCALM2) | Ratio |
|
||||
|----------|----------------------|-------------------|-------|
|
||||
| Variants | 37,294,271 | 6,473,171 | **5.8×** |
|
||||
| Bases totales | 1,415,018,593 | 408,070,894 | **3.5×** |
|
||||
| Longueur moyenne | 37.9 bases | 63.0 bases | 1.7× |
|
||||
| K-mers estimés | ~295M | ~213M | — |
|
||||
|
||||
### Stockage estimé
|
||||
|
||||
| Format | Taille estimée | Bytes/k-mer | Facteur vs v1 |
|
||||
|--------|---------------|-------------|---------------|
|
||||
| .kdi v1 (delta-varint, assemblé) | 12.8 Go | 4.3 | 1× |
|
||||
| Super-kmers 2-bit (count>1) | ~500 Mo | 1.7 | 25× |
|
||||
| **Unitigs 2-bit (BCALM2)** | **~130 Mo** | **0.6** | **98×** |
|
||||
|
||||
### Extrapolation pour l'index Contaminent_idx (Plants+Human, 2 sets)
|
||||
|
||||
Le facteur ~100× mesuré sur *Betula exilis* 15× se décompose :
|
||||
- Déréplication des reads redondants : facteur ~15× (couverture 15×)
|
||||
- Compaction super-kmer/unitig vs delta-varint : facteur ~100/15 ≈ **6.7×**
|
||||
|
||||
L'index Contaminent_idx est construit à partir de **génomes assemblés** (sans redondance de séquençage). Seul le facteur de compaction unitig s'applique :
|
||||
- Index v1 actuel : 85 Go (Plants 72 Go + Human 12.8 Go)
|
||||
- **Estimation unitigs : ~85 / 6.7 ≈ 12-13 Go** (facteur **~6.7×**)
|
||||
|
||||
C'est un gain significatif mais bien moins spectaculaire que sur des reads bruts. Le facteur pourrait être meilleur si les unitigs des génomes assemblés sont plus longs que ceux des reads (moins de fragmentation par les erreurs de séquençage).
|
||||
|
||||
### Observation sur le nombre de k-mers
|
||||
|
||||
Les unitigs contiennent ~213M k-mers vs ~295M estimés dans les super-kmers. La différence (~80M) provient probablement de k-mers qui étaient comptés dans plusieurs super-kmers (aux jonctions) et qui ne sont comptés qu'une fois dans les unitigs (déduplication exacte par le graphe de De Bruijn).
|
||||
|
||||
### Conclusion
|
||||
|
||||
L'approche unitig est massivement plus compacte que toutes les alternatives. Le format de stockage final devrait être basé sur les unitigs (ou au minimum sur les super-kmers dérepliqués) plutôt que sur des k-mers individuels en delta-varint.
|
||||
|
||||
## 10. Questions ouvertes
|
||||
|
||||
### 10.1 Le format super-kmer est-il toujours meilleur que delta-varint ?
|
||||
|
||||
D'après les estimations révisées (section 8.3), le format super-kmer 2-bit est **toujours plus compact** que le delta-varint, même pour des génomes assemblés :
|
||||
- Reads bruts 15× : ~500 Mo vs ~1.5 Go (facteur 3×, à k-mers égaux) + déréplication massive
|
||||
- Génomes assemblés : ~1.2 bytes/k-mer vs ~5 bytes/k-mer (facteur 4×)
|
||||
|
||||
La raison fondamentale : le delta-varint encode chaque k-mer indépendamment (même avec deltas), tandis que le super-kmer exploite le chevauchement de (k-1) bases entre k-mers consécutifs. C'est un avantage structurel irrattrapable par le delta-varint.
|
||||
|
||||
**Le format super-kmer semble donc préférable dans tous les cas.**
|
||||
|
||||
### 10.2 L'index doit-il stocker les super-kmers ou les k-mers ?
|
||||
|
||||
Stocker les super-kmers/unitigs comme format d'index final a des avantages (compacité, scan naturel) mais des inconvénients :
|
||||
- Pas de seek rapide vers un k-mer spécifique (vs .kdx sparse index)
|
||||
- Le matching par scan complet est O(total_bases) vs O(Q + K) pour le merge-scan
|
||||
- Les opérations ensemblistes (Union, Intersect) deviennent plus complexes
|
||||
|
||||
**Approche hybride possible** :
|
||||
1. Phase de construction : lowmask → super-kmers canoniques → déréplication → filtre de fréquence
|
||||
2. Phase de finalisation : extraire les k-mers uniques des super-kmers filtrés → delta-varint .kdi (v1 ou v2)
|
||||
3. Les super-kmers servent de **format intermédiaire efficace**, pas de format d'index final
|
||||
|
||||
Cela combine le meilleur des deux mondes :
|
||||
- Déréplication ultra-efficace au niveau super-kmer (facteur 16× sur reads bruts)
|
||||
- Index final compact et query-efficient en delta-varint
|
||||
|
||||
### 10.3 Le filtre de fréquence simple (niveau super-kmer) est-il suffisant ?
|
||||
|
||||
À valider expérimentalement :
|
||||
- Comparer le nombre de k-mers retenus par filtre super-kmer vs filtre k-mer exact
|
||||
- Mesurer l'impact sur les métriques biologiques (Jaccard, match positions)
|
||||
- Si la différence est <1%, le filtre simple suffit
|
||||
|
||||
### 10.4 Aho-Corasick vs merge-scan pour le matching final ?
|
||||
|
||||
Si le format d'index final reste delta-varint (question 9.2), le merge-scan reste la méthode naturelle de matching. L'AC/hash-set n'a d'intérêt que si le format de stockage est basé sur des séquences (unitigs/super-kmers).
|
||||
|
||||
## 11. Prochaine étape : validation expérimentale
|
||||
|
||||
Avant de modifier l'architecture, valider sur des données réelles :
|
||||
|
||||
1. **Taux de compaction super-kmer** : sur un génome assemblé vs reads bruts, mesurer le nombre de super-kmers uniques et leur longueur moyenne
|
||||
2. **Impact du filtre super-kmer** : comparer filtre au niveau super-kmer vs filtre au niveau k-mer exact sur un jeu de données de référence
|
||||
3. **Taux d'assembly en unitigs** : mesurer la longueur des unitigs obtenus à partir des super-kmers dérepliqués
|
||||
4. **Benchmark stockage** : comparer taille index super-kmer vs delta-varint vs unitig sur les mêmes données
|
||||
5. **Benchmark matching** : comparer temps de matching AC/hash vs merge-scan sur différentes densités de requêtes
|
||||
@@ -1,264 +0,0 @@
|
||||
# Optimisation du parsing des grandes séquences
|
||||
|
||||
## Contexte
|
||||
|
||||
OBITools4 doit pouvoir traiter des séquences de taille chromosomique (plusieurs Gbp), notamment
|
||||
issues de fichiers GenBank/EMBL (assemblages de génomes) ou de fichiers FASTA convertis depuis
|
||||
ces formats.
|
||||
|
||||
## Architecture actuelle
|
||||
|
||||
### Pipeline de lecture (`pkg/obiformats/`)
|
||||
|
||||
```
|
||||
ReadFileChunk (goroutine)
|
||||
→ ChannelFileChunk
|
||||
→ N × _ParseGenbankFile / _ParseFastaFile (goroutines)
|
||||
→ IBioSequence
|
||||
```
|
||||
|
||||
`ReadFileChunk` (`file_chunk_read.go`) lit le fichier par morceaux via une chaîne de
|
||||
`PieceOfChunk` (rope). Chaque nœud fait `fileChunkSize` bytes :
|
||||
|
||||
- GenBank/EMBL : 128 MB (`1024*1024*128`)
|
||||
- FASTA/FASTQ : 1 MB (`1024*1024`)
|
||||
|
||||
La chaîne est accumulée jusqu'à trouver la fin du dernier enregistrement complet (splitter),
|
||||
puis `Pack()` est appelé pour fusionner tous les nœuds en un seul buffer contigu. Ce buffer
|
||||
est transmis au parseur via `FileChunk.Raw *bytes.Buffer`.
|
||||
|
||||
### Parseur GenBank (`genbank_read.go`)
|
||||
|
||||
`GenbankChunkParser` reçoit un `io.Reader` sur le buffer packé, lit ligne par ligne via
|
||||
`bufio.NewReader` (buffer 4096 bytes), et pour chaque ligne de la section `ORIGIN` :
|
||||
|
||||
```go
|
||||
line = string(bline) // allocation par ligne
|
||||
cleanline := strings.TrimSpace(line) // allocation
|
||||
parts := strings.SplitN(cleanline, " ", 7) // allocation []string + substrings
|
||||
for i := 1; i < lparts; i++ {
|
||||
seqBytes.WriteString(parts[i])
|
||||
}
|
||||
```
|
||||
|
||||
Point positif : `seqBytes` est pré-alloué grâce à `lseq` extrait de la ligne `LOCUS`.
|
||||
|
||||
### Parseur FASTA (`fastaseq_read.go`)
|
||||
|
||||
`FastaChunkParser` lit **octet par octet** via `scanner.ReadByte()`. Pour 3 Gbp :
|
||||
3 milliards d'appels. `seqBytes` est un `bytes.Buffer{}` sans pré-allocation.
|
||||
|
||||
## Problème principal
|
||||
|
||||
Pour une séquence de plusieurs Gbp, `Pack()` fusionne une chaîne de ~N nœuds de 128 MB en
|
||||
un seul buffer contigu. C'est une allocation de N × 128 MB suivie d'une copie de toutes les
|
||||
données. Bien que l'implémentation de `Pack()` soit efficace (libère les nœuds au fur et à
|
||||
mesure via `slices.Grow`), la copie est inévitable avec l'architecture actuelle.
|
||||
|
||||
De plus, le parseur GenBank produit des dizaines de millions d'allocations temporaires pour
|
||||
parser la section `ORIGIN` (une par ligne).
|
||||
|
||||
## Invariant clé découvert
|
||||
|
||||
**Si la rope a plus d'un nœud, le premier nœud seul ne se termine pas sur une frontière
|
||||
d'enregistrement** (pas de `//\n` en fin de `piece1`).
|
||||
|
||||
Preuve par construction dans `ReadFileChunk` :
|
||||
- `splitter` est appelé dès le premier nœud (ligne 157)
|
||||
- Si `end >= 0` → frontière trouvée dans 128 MB → boucle interne sautée → rope à 1 nœud
|
||||
- Si `end < 0` → boucle interne ajoute des nœuds → rope à ≥ 2 nœuds
|
||||
|
||||
Corollaire : si rope à 1 nœud, `Pack()` ne fait rien (aucun nœud suivant).
|
||||
|
||||
**Attention** : rope à ≥ 2 nœuds ne signifie pas qu'il n'y a qu'une seule séquence dans
|
||||
la rope. La rope packée peut contenir plusieurs enregistrements complets. Exemple : records
|
||||
de 80 MB → `nextpieces` (48 MB de reste) + nouveau nœud (128 MB) = rope à 2 nœuds
|
||||
contenant 2 records complets + début d'un troisième.
|
||||
|
||||
L'invariant dit seulement que `piece1` seul est incomplet — pas que la rope entière
|
||||
ne contient qu'un seul record.
|
||||
|
||||
**Invariant : le dernier FileChunk envoyé finit sur une frontière d'enregistrement.**
|
||||
|
||||
Deux chemins dans `ReadFileChunk` :
|
||||
|
||||
1. **Chemin normal** (`end >= 0` via `splitter`) : le buffer est explicitement tronqué à
|
||||
`end` (ligne 200 : `pieces.data = pieces.data[:end]`). Frontière garantie par construction
|
||||
pour tous les formats. ✓
|
||||
|
||||
2. **Chemin EOF** (`end < 0`, `end = pieces.Len()`) : tout le reste du fichier est envoyé.
|
||||
- **GenBank/EMBL** : présuppose fichier bien formé (se termine par `//\n`). Le parseur
|
||||
lève un `log.Fatalf` sur tout état inattendu — filet de sécurité suffisant. ✓
|
||||
- **FASTQ** : présupposé, vérifié par le parseur. ✓
|
||||
- **FASTA** : garanti par le format lui-même (fin d'enregistrement = EOF ou `>`). ✓
|
||||
|
||||
**Hypothèse de travail adoptée** : les fichiers d'entrée sont bien formés. Dans le pire cas,
|
||||
le parseur lèvera une erreur explicite. Il n'y a pas de risque de corruption silencieuse.
|
||||
|
||||
## Piste d'optimisation : se dispenser de Pack()
|
||||
|
||||
### Idée centrale
|
||||
|
||||
Au lieu de fusionner la rope avant de la passer au parseur, **parser directement la rope
|
||||
nœud par nœud**, et **écrire la séquence compactée in-place dans le premier nœud**.
|
||||
|
||||
Pourquoi c'est sûr :
|
||||
- Le header (LOCUS, DEFINITION, SOURCE, FEATURES) est **petit** et traité en premier
|
||||
- La séquence (ORIGIN) est **à la fin** du record
|
||||
- Au moment d'écrire la séquence depuis l'offset 0 de `piece1`, le pointeur de lecture
|
||||
est profond dans la rope (offset >> 0) → jamais de collision
|
||||
- La séquence compactée est toujours plus courte que les données brutes
|
||||
|
||||
### Pré-allocation
|
||||
|
||||
Pour GenBank/EMBL : `lseq` est connu dès la ligne `LOCUS`/`ID` (première ligne, dans
|
||||
`piece1`). On peut faire `slices.Grow(piece1.data, lseq)` dès ce moment.
|
||||
|
||||
Pour FASTA : pas de taille garantie dans le header, mais `rope.Len()` donne un majorant.
|
||||
On peut utiliser `rope.Len() / 2` comme estimation initiale.
|
||||
|
||||
### Gestion des jonctions entre nœuds
|
||||
|
||||
Une ligne peut chevaucher deux nœuds (rare avec 128 MB, mais possible). Solution : carry
|
||||
buffer de ~128 bytes pour les quelques bytes en fin de nœud.
|
||||
|
||||
### Cas FASTA/FASTQ multi-séquences
|
||||
|
||||
Un FileChunk peut contenir N séquences (notamment FASTA/FASTQ courts). Dans ce cas
|
||||
l'écriture in-place dans `piece1` n'est pas applicable directement — on écrase des données
|
||||
nécessaires aux séquences suivantes.
|
||||
|
||||
Stratégie par cas :
|
||||
- **Rope à 1 nœud** (record ≤ 128 MB) : `Pack()` est trivial (no-op), parseur actuel OK
|
||||
- **Rope à ≥ 2 nœuds** : par l'invariant, `piece1` ne contient pas de record complet →
|
||||
une seule grande séquence → in-place applicable
|
||||
|
||||
### Format d'une ligne séquence GenBank (Après ORIGIN)
|
||||
|
||||
```
|
||||
/^ *[0-9]+( [nuc]{10}){0,5} [nuc]{1,10}/
|
||||
```
|
||||
|
||||
### Format d'une ligne séquence GenBank (Après SQ)
|
||||
|
||||
La ligne SQ contient aussi la taille de la séquence
|
||||
|
||||
```
|
||||
/^ *( [nuc]{10}){0,5} [nuc]{1,10} *[0-9]+/
|
||||
```
|
||||
|
||||
Compactage in-place sur `bline` ([]byte brut, sans conversion `string`) :
|
||||
|
||||
```go
|
||||
w := 0
|
||||
i := 0
|
||||
for i < len(bline) && bline[i] == ' ' { i++ } // skip indentation
|
||||
for i < len(bline) && bline[i] <= '9' { i++ } // skip position number
|
||||
for ; i < len(bline); i++ {
|
||||
if bline[i] != ' ' {
|
||||
bline[w] = bline[i]
|
||||
w++
|
||||
}
|
||||
}
|
||||
// écrire bline[:w] directement dans piece1.data[seqOffset:]
|
||||
```
|
||||
|
||||
## Changements nécessaires
|
||||
|
||||
1. **`FileChunk`** : exposer la rope `*PieceOfChunk` non-packée en plus (ou à la place)
|
||||
de `Raw *bytes.Buffer`
|
||||
2. **`GenbankChunkParser` / `EmblChunkParser`** : accepter `*PieceOfChunk`, parser la
|
||||
rope séquentiellement avec carry buffer pour les jonctions
|
||||
3. **`FastaChunkParser`** : idem, avec in-place conditionnel selon taille de la rope
|
||||
4. **`ReadFileChunk`** : ne pas appeler `Pack()` avant envoi sur le channel (ou version
|
||||
alternative `ReadFileChunkRope`)
|
||||
|
||||
## Fichiers concernés
|
||||
|
||||
- `pkg/obiformats/file_chunk_read.go` — structure rope, `ReadFileChunk`
|
||||
- `pkg/obiformats/genbank_read.go` — `GenbankChunkParser`, `_ParseGenbankFile`
|
||||
- `pkg/obiformats/embl_read.go` — `EmblChunkParser`, `ReadEMBL`
|
||||
- `pkg/obiformats/fastaseq_read.go` — `FastaChunkParser`, `_ParseFastaFile`
|
||||
- `pkg/obiformats/fastqseq_read.go` — parseur FASTQ (même structure)
|
||||
|
||||
## Plan d'implémentation : parseur GenBank sur rope
|
||||
|
||||
### Contexte
|
||||
|
||||
Baseline mesurée : `obiconvert gbpln640.seq.gz` → 49s real, 42s user, 29s sys, **57 GB RSS**.
|
||||
Le sys élevé indique des allocations massives. Deux causes :
|
||||
1. `Pack()` : fusionne toute la rope (N × 128 MB) en un buffer contigu avant de parser
|
||||
2. Parser ORIGIN : `string(bline)` + `TrimSpace` + `SplitN` × millions de lignes
|
||||
|
||||
### 1. `gbRopeScanner`
|
||||
|
||||
Struct de lecture ligne par ligne sur la rope, sans allocation heap :
|
||||
|
||||
```go
|
||||
type gbRopeScanner struct {
|
||||
current *PieceOfChunk
|
||||
pos int
|
||||
carry [256]byte // stack-allocated, max GenBank line = 80 chars
|
||||
carryN int
|
||||
}
|
||||
```
|
||||
|
||||
`ReadLine()` :
|
||||
- Cherche `\n` dans `current.data[pos:]` via `bytes.IndexByte`
|
||||
- Si trouvé sans carry : retourne slice direct du node (zéro alloc)
|
||||
- Si trouvé avec carry : copie dans carry buffer, retourne `carry[:n]`
|
||||
- Si non trouvé : copie le reste dans carry, avance au node suivant, recommence
|
||||
- EOF : retourne `carry[:carryN]` puis nil
|
||||
|
||||
`extractSequence(dest []byte, UtoT bool) int` :
|
||||
- Scan direct des bytes pour section ORIGIN, sans passer par ReadLine
|
||||
- Machine d'états : lineStart → skip espaces/digits → copier nucléotides dans dest
|
||||
- Stop sur `//` en début de ligne
|
||||
- Zéro allocation, UtoT inline
|
||||
|
||||
### 2. `GenbankChunkParserRope`
|
||||
|
||||
```go
|
||||
func GenbankChunkParserRope(source string, rope *PieceOfChunk,
|
||||
withFeatureTable, UtoT bool) (obiseq.BioSequenceSlice, error)
|
||||
```
|
||||
|
||||
- Même machine d'états que `GenbankChunkParser`, sur `[]byte` (`bytes.HasPrefix`)
|
||||
- LOCUS : extrait `id` et `lseq` par scan direct (remplace `_seqlenght_rx`)
|
||||
- FEATURES / default inFeature : taxid extrait par scan de `/db_xref="taxon:`
|
||||
dans la source feature ; `featBytes` rempli seulement si `withFeatureTable=true`
|
||||
- DEFINITION : toujours conservée
|
||||
- ORIGIN : `dest = make([]byte, 0, lseq+20)` puis `s.extractSequence(dest, UtoT)`
|
||||
|
||||
### 3. Modifications `_ParseGenbankFile` et `ReadGenbank`
|
||||
|
||||
`_ParseGenbankFile` utilise `chunk.Rope` :
|
||||
```go
|
||||
sequences, err := GenbankChunkParserRope(chunk.Source, chunk.Rope, ...)
|
||||
```
|
||||
|
||||
`ReadGenbank` passe `pack=false` :
|
||||
```go
|
||||
entry_channel := ReadFileChunk(..., false)
|
||||
```
|
||||
|
||||
### 4. Ce qui NE change pas
|
||||
|
||||
- `GenbankChunkParser` reste (référence, tests)
|
||||
- `ReadFileChunk`, `Pack()`, autres parseurs (EMBL, FASTA, FASTQ) : inchangés
|
||||
|
||||
### 5. Gains attendus
|
||||
|
||||
- **RSS** : pic ≈ 128 MB × workers (au lieu de N × 128 MB)
|
||||
- **Temps sys** : élimination des mmap/munmap pour les gros buffers
|
||||
- **Temps user** : ~50M allocations éliminées
|
||||
|
||||
### 6. Vérification
|
||||
|
||||
```bash
|
||||
/usr/local/go/bin/go build ./...
|
||||
diff <(obiconvert gbpln640.seq.gz) gbpln640.reference.fasta
|
||||
cd bugs/genbank && ./benchmark.sh gbpln640.seq.gz
|
||||
```
|
||||
|
||||
Cible : RSS < 1 GB, temps comparable ou meilleur.
|
||||
3
blackboard/ToDo/Canonical-superkmers.md
Normal file
3
blackboard/ToDo/Canonical-superkmers.md
Normal file
@@ -0,0 +1,3 @@
|
||||
lit le ficier [@canonical-super-kmer-strategy.md](file:///Users/coissac/Sync/travail/__MOI__/GO/obitools4/blackboard/Prospective/canonical-super-kmer-strategy.md).
|
||||
|
||||
Dans le fichier [@superkmer_iter.go](file:///Users/coissac/Sync/travail/__MOI__/GO/obitools4/pkg/obikmer/superkmer_iter.go) implemente une nouvelle fonction IterCanonicalSuperKmers sur le modèle de IterSuperKmers, qui implémente la notion de SuperKmers canonique présenté dans le document d'architecture.
|
||||
41
go.mod
41
go.mod
@@ -1,33 +1,35 @@
|
||||
module git.metabarcoding.org/obitools/obitools4/obitools4
|
||||
|
||||
go 1.26.1
|
||||
go 1.23.4
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/DavidGamba/go-getoptions v0.33.0
|
||||
github.com/PaesslerAG/gval v1.2.4
|
||||
github.com/DavidGamba/go-getoptions v0.28.0
|
||||
github.com/PaesslerAG/gval v1.2.2
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/chen3feng/stl4go v0.1.1
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/goccy/go-json v0.10.6
|
||||
github.com/dlclark/regexp2 v1.11.4
|
||||
github.com/goccy/go-json v0.10.3
|
||||
github.com/klauspost/pgzip v1.2.6
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/rrethy/ahocorasick v1.0.0
|
||||
github.com/schollz/progressbar/v3 v3.19.0
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/schollz/progressbar/v3 v3.13.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tevino/abool/v2 v2.1.0
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
|
||||
gonum.org/v1/gonum v0.17.0
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
|
||||
gonum.org/v1/gonum v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
scientificgo.org/special v0.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/goombaio/orderedmap v0.0.0-20180925151256-3da0e2f905f9 // indirect
|
||||
github.com/goombaio/orderedmap v0.0.0-20180924084748-ba921b7e2419 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
@@ -36,15 +38,16 @@ require (
|
||||
|
||||
require (
|
||||
github.com/dsnet/compress v0.0.1
|
||||
github.com/gabriel-vasile/mimetype v1.4.13
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/goombaio/orderedset v0.0.0-20180925151225-8e67b20a9b77
|
||||
github.com/klauspost/compress v1.18.4
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/klauspost/compress v1.17.2
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||
)
|
||||
|
||||
91
go.sum
91
go.sum
@@ -1,7 +1,7 @@
|
||||
github.com/DavidGamba/go-getoptions v0.33.0 h1:8xCPH87Yy5avYenygyHVlqqm8RpymH0YFe4a7IWlarE=
|
||||
github.com/DavidGamba/go-getoptions v0.33.0/go.mod h1:zE97E3PR9P3BI/HKyNYgdMlYxodcuiC6W68KIgeYT84=
|
||||
github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU=
|
||||
github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
|
||||
github.com/DavidGamba/go-getoptions v0.28.0 h1:18wgEvfZdrlfIhVDGEBO3Dl0fkOyXqXLa0tLMCKxM1c=
|
||||
github.com/DavidGamba/go-getoptions v0.28.0/go.mod h1:zE97E3PR9P3BI/HKyNYgdMlYxodcuiC6W68KIgeYT84=
|
||||
github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E=
|
||||
github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
||||
github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0=
|
||||
@@ -10,32 +10,27 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMU
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/chen3feng/stl4go v0.1.1 h1:0L1+mDw7pomftKDruM23f1mA7miavOj6C6MZeadzN2Q=
|
||||
github.com/chen3feng/stl4go v0.1.1/go.mod h1:5ml3psLgETJjRJnMbPE+JiHLrCpt+Ajc2weeTECXzWU=
|
||||
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
|
||||
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
|
||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goombaio/orderedmap v0.0.0-20180924084748-ba921b7e2419 h1:SajEQ6tktpF9SRIuzbiPOX9AEZZ53Bvw0k9Mzrts8Lg=
|
||||
github.com/goombaio/orderedmap v0.0.0-20180924084748-ba921b7e2419/go.mod h1:YKu81H3RSd1cFh0d7NhvUoTtUC9IY/vBX0WUQb1/o4Y=
|
||||
github.com/goombaio/orderedmap v0.0.0-20180925151256-3da0e2f905f9 h1:vFjPvFavIiDY71bQ9HIxPQBANvNl1SmFC4fgg5xRkho=
|
||||
github.com/goombaio/orderedmap v0.0.0-20180925151256-3da0e2f905f9/go.mod h1:YKu81H3RSd1cFh0d7NhvUoTtUC9IY/vBX0WUQb1/o4Y=
|
||||
github.com/goombaio/orderedset v0.0.0-20180925151225-8e67b20a9b77 h1:4dvq1tGHn1Y9KSRY0OZ24Khki4+4U+ZrA//YYsdUlJU=
|
||||
github.com/goombaio/orderedset v0.0.0-20180925151225-8e67b20a9b77/go.mod h1:HPelMYpOyy0XvglpBbmZ3krZpwaHmszj/vQNlnETPTM=
|
||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
|
||||
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
@@ -46,8 +41,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
@@ -57,40 +54,50 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rrethy/ahocorasick v1.0.0 h1:YKkCB+E5PXc0xmLfMrWbfNht8vG9Re97IHSWZk/Lk8E=
|
||||
github.com/rrethy/ahocorasick v1.0.0/go.mod h1:nq8oScE7Vy1rOppoQxpQiiDmPHuKCuk9rXrNcxUV3R0=
|
||||
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
||||
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE=
|
||||
github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c=
|
||||
github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
gonum.org/v1/gonum v0.14.0 h1:2NiG67LD1tEH0D7kM+ps2V+fXmsAnpUeec7n8tcr4S0=
|
||||
gonum.org/v1/gonum v0.14.0/go.mod h1:AoWeoz0becf9QMWtE8iWXNXc27fK4fNeHNf/oMejGfU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
scientificgo.org/special v0.0.0 h1:P6WJkECo6tgtvZAEfNXl+KEB9ReAatjKAeX8U07mjSc=
|
||||
|
||||
@@ -52,7 +52,6 @@ golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
|
||||
@@ -7,7 +7,6 @@ INSTALL_DIR="/usr/local"
|
||||
OBITOOLS_PREFIX=""
|
||||
VERSION=""
|
||||
LIST_VERSIONS=false
|
||||
JOBS=1
|
||||
|
||||
# Help message
|
||||
function display_help {
|
||||
@@ -22,7 +21,6 @@ function display_help {
|
||||
echo " gobigrep command instead of obigrep)."
|
||||
echo " -v, --version Install a specific version (e.g., 4.4.8)."
|
||||
echo " If not specified, installs the latest version."
|
||||
echo " -j, --jobs Number of parallel jobs for compilation (default: 1)."
|
||||
echo " -l, --list List all available versions and exit."
|
||||
echo " -h, --help Display this help message."
|
||||
echo ""
|
||||
@@ -67,10 +65,6 @@ while [ "$#" -gt 0 ]; do
|
||||
VERSION="$2"
|
||||
shift 2
|
||||
;;
|
||||
-j|--jobs)
|
||||
JOBS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-l|--list)
|
||||
LIST_VERSIONS=true
|
||||
shift
|
||||
@@ -128,15 +122,9 @@ mkdir -p "${WORK_DIR}/cache" \
|
||||
exit 1)
|
||||
|
||||
# Create installation directory
|
||||
if ! mkdir -p "${INSTALL_DIR}/bin" 2>/dev/null; then
|
||||
if [ ! -w "$(dirname "${INSTALL_DIR}")" ] && [ ! -w "${INSTALL_DIR}" ]; then
|
||||
echo "Please enter your password for installing obitools in ${INSTALL_DIR}" 1>&2
|
||||
sudo mkdir -p "${INSTALL_DIR}/bin"
|
||||
else
|
||||
echo "Error: Could not create ${INSTALL_DIR}/bin (check path or disk space)" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
mkdir -p "${INSTALL_DIR}/bin" 2> /dev/null \
|
||||
|| (echo "Please enter your password for installing obitools in ${INSTALL_DIR}" 1>&2
|
||||
sudo mkdir -p "${INSTALL_DIR}/bin")
|
||||
|
||||
if [[ ! -d "${INSTALL_DIR}/bin" ]]; then
|
||||
echo "Could not create ${INSTALL_DIR}/bin directory for installing obitools" 1>&2
|
||||
@@ -183,24 +171,22 @@ GOURL=$(curl -s "${URL}${GOFILE}" \
|
||||
|
||||
echo "Installing Go from: $GOURL" 1>&2
|
||||
|
||||
curl --progress-bar "$GOURL" | tar zxf -
|
||||
curl -s "$GOURL" | tar zxf -
|
||||
|
||||
export GOROOT="$(pwd)/go"
|
||||
PATH="${GOROOT}/bin:$PATH"
|
||||
PATH="$(pwd)/go/bin:$PATH"
|
||||
export PATH
|
||||
export GOPATH="$(pwd)/gopath"
|
||||
GOPATH="$(pwd)/go"
|
||||
export GOPATH
|
||||
export GOCACHE="$(pwd)/cache"
|
||||
export GOTOOLCHAIN=local
|
||||
|
||||
echo "GOROOT=$GOROOT" 1>&2
|
||||
echo "GOCACHE=$GOCACHE" 1>&2
|
||||
mkdir -p "$GOPATH" "$GOCACHE"
|
||||
mkdir -p "$GOCACHE"
|
||||
|
||||
# Download OBITools4 source
|
||||
echo "Downloading OBITools4 v${VERSION}..." 1>&2
|
||||
echo "Source URL: $OBIURL4" 1>&2
|
||||
|
||||
if ! curl --progress-bar -L "$OBIURL4" > obitools4.zip; then
|
||||
if ! curl -sL "$OBIURL4" > obitools4.zip; then
|
||||
echo "Error: Could not download OBITools4 version ${VERSION}" 1>&2
|
||||
echo "Please check that this version exists with: $0 --list" 1>&2
|
||||
exit 1
|
||||
@@ -222,29 +208,16 @@ mkdir -p vendor
|
||||
|
||||
# Build with or without prefix
|
||||
if [[ -z "$OBITOOLS_PREFIX" ]] ; then
|
||||
make -j"${JOBS}" obitools GOFLAGS="-buildvcs=false"
|
||||
make GOFLAGS="-buildvcs=false"
|
||||
else
|
||||
make -j"${JOBS}" obitools GOFLAGS="-buildvcs=false" OBITOOLS_PREFIX="${OBITOOLS_PREFIX}"
|
||||
make GOFLAGS="-buildvcs=false" OBITOOLS_PREFIX="${OBITOOLS_PREFIX}"
|
||||
fi
|
||||
|
||||
# Install binaries
|
||||
echo "Installing binaries to ${INSTALL_DIR}/bin..." 1>&2
|
||||
if ! cp build/* "${INSTALL_DIR}/bin" 2>/dev/null; then
|
||||
if [ ! -w "${INSTALL_DIR}/bin" ]; then
|
||||
echo "Please enter your password for installing obitools in ${INSTALL_DIR}" 1>&2
|
||||
sudo cp build/* "${INSTALL_DIR}/bin"
|
||||
else
|
||||
echo "Error: Could not copy binaries to ${INSTALL_DIR}/bin" 1>&2
|
||||
echo " Source files: $(ls build/ 2>/dev/null || echo 'none found')" 1>&2
|
||||
echo "" 1>&2
|
||||
echo "The build directory has been preserved for manual recovery:" 1>&2
|
||||
echo " $(pwd)/build/" 1>&2
|
||||
echo "You can install manually with:" 1>&2
|
||||
echo " cp $(pwd)/build/* ${INSTALL_DIR}/bin/" 1>&2
|
||||
popd > /dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
(cp build/* "${INSTALL_DIR}/bin" 2> /dev/null) \
|
||||
|| (echo "Please enter your password for installing obitools in ${INSTALL_DIR}" 1>&2
|
||||
sudo cp build/* "${INSTALL_DIR}/bin")
|
||||
|
||||
popd > /dev/null || exit
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,12 +1,6 @@
|
||||
package obidefault
|
||||
|
||||
// _BatchSize is the minimum number of sequences per batch (floor).
|
||||
// Used as the minSeqs argument to RebatchBySize.
|
||||
var _BatchSize = 1
|
||||
|
||||
// _BatchSizeMax is the maximum number of sequences per batch (ceiling).
|
||||
// A batch is flushed when this count is reached regardless of memory usage.
|
||||
var _BatchSizeMax = 2000
|
||||
var _BatchSize = 2000
|
||||
|
||||
// SetBatchSize sets the size of the sequence batches.
|
||||
//
|
||||
@@ -30,42 +24,3 @@ func BatchSize() int {
|
||||
func BatchSizePtr() *int {
|
||||
return &_BatchSize
|
||||
}
|
||||
|
||||
// BatchSizeMax returns the maximum number of sequences per batch.
|
||||
func BatchSizeMax() int {
|
||||
return _BatchSizeMax
|
||||
}
|
||||
|
||||
func BatchSizeMaxPtr() *int {
|
||||
return &_BatchSizeMax
|
||||
}
|
||||
|
||||
// _BatchMem holds the maximum cumulative memory (in bytes) per batch when
|
||||
// memory-based batching is requested. A value of 0 disables memory-based
|
||||
// batching and falls back to count-based batching.
|
||||
var _BatchMem = 128 * 1024 * 1024 // 128 MB default; set to 0 to disable
|
||||
var _BatchMemStr = ""
|
||||
|
||||
// SetBatchMem sets the memory budget per batch in bytes.
|
||||
func SetBatchMem(n int) {
|
||||
_BatchMem = n
|
||||
}
|
||||
|
||||
// BatchMem returns the current memory budget per batch in bytes.
|
||||
// A value of 0 means memory-based batching is disabled.
|
||||
func BatchMem() int {
|
||||
return _BatchMem
|
||||
}
|
||||
|
||||
func BatchMemPtr() *int {
|
||||
return &_BatchMem
|
||||
}
|
||||
|
||||
// BatchMemStr returns the raw --batch-mem string value as provided on the CLI.
|
||||
func BatchMemStr() string {
|
||||
return _BatchMemStr
|
||||
}
|
||||
|
||||
func BatchMemStrPtr() *string {
|
||||
return &_BatchMemStr
|
||||
}
|
||||
|
||||
@@ -161,149 +161,6 @@ func EmblChunkParser(withFeatureTable, UtoT bool) func(string, io.Reader) (obise
|
||||
return parser
|
||||
}
|
||||
|
||||
// extractEmblSeq scans the sequence section of an EMBL record directly on the
|
||||
// rope. EMBL sequence lines start with 5 spaces followed by bases in groups of
|
||||
// 10, separated by spaces, with a position number at the end. The section ends
|
||||
// with "//".
|
||||
func (s *ropeScanner) extractEmblSeq(dest []byte, UtoT bool) []byte {
|
||||
// We use ReadLine and scan each line for bases (skip digits, spaces, newlines).
|
||||
for {
|
||||
line := s.ReadLine()
|
||||
if line == nil {
|
||||
break
|
||||
}
|
||||
if len(line) >= 2 && line[0] == '/' && line[1] == '/' {
|
||||
break
|
||||
}
|
||||
// Lines start with 5 spaces; bases follow separated by single spaces.
|
||||
// Digits at the end are the position counter — skip them.
|
||||
// Simplest: take every byte that is a letter.
|
||||
for _, b := range line {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
b += 'a' - 'A'
|
||||
}
|
||||
if UtoT && b == 'u' {
|
||||
b = 't'
|
||||
}
|
||||
if b >= 'a' && b <= 'z' {
|
||||
dest = append(dest, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// EmblChunkParserRope parses an EMBL chunk directly from a rope without Pack().
|
||||
func EmblChunkParserRope(source string, rope *PieceOfChunk, withFeatureTable, UtoT bool) (obiseq.BioSequenceSlice, error) {
|
||||
scanner := newRopeScanner(rope)
|
||||
sequences := obiseq.MakeBioSequenceSlice(100)[:0]
|
||||
|
||||
var id string
|
||||
var scientificName string
|
||||
defBytes := make([]byte, 0, 256)
|
||||
featBytes := make([]byte, 0, 1024)
|
||||
var taxid int
|
||||
inSeq := false
|
||||
|
||||
for {
|
||||
line := scanner.ReadLine()
|
||||
if line == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if inSeq {
|
||||
// Should not happen — extractEmblSeq consumed up to "//"
|
||||
inSeq = false
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case bytes.HasPrefix(line, []byte("ID ")):
|
||||
id = string(bytes.SplitN(line[5:], []byte(";"), 2)[0])
|
||||
case bytes.HasPrefix(line, []byte("OS ")):
|
||||
scientificName = string(bytes.TrimSpace(line[5:]))
|
||||
case bytes.HasPrefix(line, []byte("DE ")):
|
||||
if len(defBytes) > 0 {
|
||||
defBytes = append(defBytes, ' ')
|
||||
}
|
||||
defBytes = append(defBytes, bytes.TrimSpace(line[5:])...)
|
||||
case withFeatureTable && bytes.HasPrefix(line, []byte("FH ")):
|
||||
featBytes = append(featBytes, line...)
|
||||
case withFeatureTable && bytes.Equal(line, []byte("FH")):
|
||||
featBytes = append(featBytes, '\n')
|
||||
featBytes = append(featBytes, line...)
|
||||
case bytes.HasPrefix(line, []byte("FT ")):
|
||||
if withFeatureTable {
|
||||
featBytes = append(featBytes, '\n')
|
||||
featBytes = append(featBytes, line...)
|
||||
}
|
||||
if bytes.HasPrefix(line, []byte(`FT /db_xref="taxon:`)) {
|
||||
rest := line[37:]
|
||||
end := bytes.IndexByte(rest, '"')
|
||||
if end > 0 {
|
||||
taxid, _ = strconv.Atoi(string(rest[:end]))
|
||||
}
|
||||
}
|
||||
case bytes.HasPrefix(line, []byte(" ")):
|
||||
// First sequence line: extract all bases via extractEmblSeq,
|
||||
// which also consumes this line's remaining content.
|
||||
// But ReadLine already consumed this line — we need to process it
|
||||
// plus subsequent lines. Process this line inline then call helper.
|
||||
seqDest := make([]byte, 0, 4096)
|
||||
for _, b := range line {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
b += 'a' - 'A'
|
||||
}
|
||||
if UtoT && b == 'u' {
|
||||
b = 't'
|
||||
}
|
||||
if b >= 'a' && b <= 'z' {
|
||||
seqDest = append(seqDest, b)
|
||||
}
|
||||
}
|
||||
seqDest = scanner.extractEmblSeq(seqDest, UtoT)
|
||||
|
||||
seq := obiseq.NewBioSequenceOwning(id, seqDest, string(defBytes))
|
||||
seq.SetSource(source)
|
||||
if withFeatureTable {
|
||||
seq.SetFeatures(featBytes)
|
||||
}
|
||||
annot := seq.Annotations()
|
||||
annot["scientific_name"] = scientificName
|
||||
annot["taxid"] = taxid
|
||||
sequences = append(sequences, seq)
|
||||
|
||||
// Reset state
|
||||
id = ""
|
||||
scientificName = ""
|
||||
defBytes = defBytes[:0]
|
||||
featBytes = featBytes[:0]
|
||||
taxid = 1
|
||||
|
||||
case bytes.Equal(line, []byte("//")):
|
||||
// record ended without SQ/sequence section (e.g. WGS entries)
|
||||
if id != "" {
|
||||
seq := obiseq.NewBioSequenceOwning(id, []byte{}, string(defBytes))
|
||||
seq.SetSource(source)
|
||||
if withFeatureTable {
|
||||
seq.SetFeatures(featBytes)
|
||||
}
|
||||
annot := seq.Annotations()
|
||||
annot["scientific_name"] = scientificName
|
||||
annot["taxid"] = taxid
|
||||
sequences = append(sequences, seq)
|
||||
}
|
||||
id = ""
|
||||
scientificName = ""
|
||||
defBytes = defBytes[:0]
|
||||
featBytes = featBytes[:0]
|
||||
taxid = 1
|
||||
}
|
||||
}
|
||||
|
||||
return sequences, nil
|
||||
}
|
||||
|
||||
func _ParseEmblFile(
|
||||
input ChannelFileChunk,
|
||||
out obiiter.IBioSequence,
|
||||
@@ -314,14 +171,7 @@ func _ParseEmblFile(
|
||||
|
||||
for chunks := range input {
|
||||
order := chunks.Order
|
||||
var sequences obiseq.BioSequenceSlice
|
||||
var err error
|
||||
|
||||
if chunks.Rope != nil {
|
||||
sequences, err = EmblChunkParserRope(chunks.Source, chunks.Rope, withFeatureTable, UtoT)
|
||||
} else {
|
||||
sequences, err = parser(chunks.Source, chunks.Raw)
|
||||
}
|
||||
sequences, err := parser(chunks.Source, chunks.Raw)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("%s : Cannot parse the embl file : %v", chunks.Source, err)
|
||||
@@ -346,7 +196,6 @@ func ReadEMBL(reader io.Reader, options ...WithOption) (obiiter.IBioSequence, er
|
||||
1024*1024*128,
|
||||
EndOfLastFlatFileEntry,
|
||||
"\nID ",
|
||||
false,
|
||||
)
|
||||
|
||||
newIter := obiiter.MakeIBioSequence()
|
||||
|
||||
@@ -209,121 +209,28 @@ func FastaChunkParser(UtoT bool) func(string, io.Reader) (obiseq.BioSequenceSlic
|
||||
return parser
|
||||
}
|
||||
|
||||
// extractFastaSeq scans sequence bytes from the rope directly into dest,
|
||||
// appending valid nucleotide characters and skipping whitespace.
|
||||
// Stops when '>' is found at the start of a line (next record) or at EOF.
|
||||
// Returns (dest with appended bases, hasMore).
|
||||
// hasMore=true means scanner is now positioned at '>' of the next record.
|
||||
func (s *ropeScanner) extractFastaSeq(dest []byte, UtoT bool) ([]byte, bool) {
|
||||
lineStart := true
|
||||
|
||||
for s.current != nil {
|
||||
data := s.current.data[s.pos:]
|
||||
for i, b := range data {
|
||||
if lineStart && b == '>' {
|
||||
s.pos += i
|
||||
if s.pos >= len(s.current.data) {
|
||||
s.current = s.current.Next()
|
||||
s.pos = 0
|
||||
}
|
||||
return dest, true
|
||||
}
|
||||
if b == '\n' || b == '\r' {
|
||||
lineStart = true
|
||||
continue
|
||||
}
|
||||
lineStart = false
|
||||
if b == ' ' || b == '\t' {
|
||||
continue
|
||||
}
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
b += 'a' - 'A'
|
||||
}
|
||||
if UtoT && b == 'u' {
|
||||
b = 't'
|
||||
}
|
||||
dest = append(dest, b)
|
||||
}
|
||||
s.current = s.current.Next()
|
||||
s.pos = 0
|
||||
}
|
||||
return dest, false
|
||||
}
|
||||
|
||||
// FastaChunkParserRope parses a FASTA chunk directly from the rope without Pack().
|
||||
func FastaChunkParserRope(source string, rope *PieceOfChunk, UtoT bool) (obiseq.BioSequenceSlice, error) {
|
||||
scanner := newRopeScanner(rope)
|
||||
sequences := obiseq.MakeBioSequenceSlice(100)[:0]
|
||||
|
||||
for {
|
||||
bline := scanner.ReadLine()
|
||||
if bline == nil {
|
||||
break
|
||||
}
|
||||
if len(bline) == 0 || bline[0] != '>' {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse header: ">id definition"
|
||||
header := bline[1:]
|
||||
var id string
|
||||
var definition string
|
||||
sp := bytes.IndexByte(header, ' ')
|
||||
if sp < 0 {
|
||||
sp = bytes.IndexByte(header, '\t')
|
||||
}
|
||||
if sp < 0 {
|
||||
id = string(header)
|
||||
} else {
|
||||
id = string(header[:sp])
|
||||
definition = string(bytes.TrimSpace(header[sp+1:]))
|
||||
}
|
||||
|
||||
seqDest := make([]byte, 0, 4096)
|
||||
var hasMore bool
|
||||
seqDest, hasMore = scanner.extractFastaSeq(seqDest, UtoT)
|
||||
|
||||
if len(seqDest) == 0 {
|
||||
log.Fatalf("%s [%s]: sequence is empty", source, id)
|
||||
}
|
||||
|
||||
seq := obiseq.NewBioSequenceOwning(id, seqDest, definition)
|
||||
seq.SetSource(source)
|
||||
sequences = append(sequences, seq)
|
||||
|
||||
if !hasMore {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return sequences, nil
|
||||
}
|
||||
|
||||
func _ParseFastaFile(
|
||||
input ChannelFileChunk,
|
||||
out obiiter.IBioSequence,
|
||||
UtoT bool,
|
||||
) {
|
||||
|
||||
parser := FastaChunkParser(UtoT)
|
||||
|
||||
for chunks := range input {
|
||||
var sequences obiseq.BioSequenceSlice
|
||||
var err error
|
||||
|
||||
if chunks.Rope != nil {
|
||||
sequences, err = FastaChunkParserRope(chunks.Source, chunks.Rope, UtoT)
|
||||
} else {
|
||||
sequences, err = parser(chunks.Source, chunks.Raw)
|
||||
}
|
||||
sequences, err := parser(chunks.Source, chunks.Raw)
|
||||
// obilog.Warnf("Chunck(%d:%d) -%d- ", chunks.Order, l, sequences.Len())
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("File %s : Cannot parse the fasta file : %v", chunks.Source, err)
|
||||
}
|
||||
|
||||
out.Push(obiiter.MakeBioSequenceBatch(chunks.Source, chunks.Order, sequences))
|
||||
|
||||
}
|
||||
|
||||
out.Done()
|
||||
|
||||
}
|
||||
|
||||
func ReadFasta(reader io.Reader, options ...WithOption) (obiiter.IBioSequence, error) {
|
||||
@@ -338,7 +245,6 @@ func ReadFasta(reader io.Reader, options ...WithOption) (obiiter.IBioSequence, e
|
||||
1024*1024,
|
||||
EndOfLastFastaEntry,
|
||||
"\n>",
|
||||
false,
|
||||
)
|
||||
|
||||
for i := 0; i < nworker; i++ {
|
||||
|
||||
@@ -303,80 +303,6 @@ func FastqChunkParser(quality_shift byte, with_quality bool, UtoT bool) func(str
|
||||
return parser
|
||||
}
|
||||
|
||||
// FastqChunkParserRope parses a FASTQ chunk directly from a rope without Pack().
|
||||
func FastqChunkParserRope(source string, rope *PieceOfChunk, quality_shift byte, with_quality, UtoT bool) (obiseq.BioSequenceSlice, error) {
|
||||
scanner := newRopeScanner(rope)
|
||||
sequences := obiseq.MakeBioSequenceSlice(100)[:0]
|
||||
|
||||
for {
|
||||
// Line 1: @id [definition]
|
||||
hline := scanner.ReadLine()
|
||||
if hline == nil {
|
||||
break
|
||||
}
|
||||
if len(hline) == 0 || hline[0] != '@' {
|
||||
continue
|
||||
}
|
||||
header := hline[1:]
|
||||
var id string
|
||||
var definition string
|
||||
sp := bytes.IndexByte(header, ' ')
|
||||
if sp < 0 {
|
||||
sp = bytes.IndexByte(header, '\t')
|
||||
}
|
||||
if sp < 0 {
|
||||
id = string(header)
|
||||
} else {
|
||||
id = string(header[:sp])
|
||||
definition = string(bytes.TrimSpace(header[sp+1:]))
|
||||
}
|
||||
|
||||
// Line 2: sequence
|
||||
sline := scanner.ReadLine()
|
||||
if sline == nil {
|
||||
log.Fatalf("@%s[%s]: unexpected EOF after header", id, source)
|
||||
}
|
||||
seqDest := make([]byte, len(sline))
|
||||
w := 0
|
||||
for _, b := range sline {
|
||||
if b >= 'A' && b <= 'Z' {
|
||||
b += 'a' - 'A'
|
||||
}
|
||||
if UtoT && b == 'u' {
|
||||
b = 't'
|
||||
}
|
||||
seqDest[w] = b
|
||||
w++
|
||||
}
|
||||
seqDest = seqDest[:w]
|
||||
if len(seqDest) == 0 {
|
||||
log.Fatalf("@%s[%s]: sequence is empty", id, source)
|
||||
}
|
||||
|
||||
// Line 3: + (skip)
|
||||
scanner.ReadLine()
|
||||
|
||||
// Line 4: quality
|
||||
qline := scanner.ReadLine()
|
||||
|
||||
seq := obiseq.NewBioSequenceOwning(id, seqDest, definition)
|
||||
seq.SetSource(source)
|
||||
|
||||
if with_quality && qline != nil {
|
||||
qDest := make([]byte, len(qline))
|
||||
copy(qDest, qline)
|
||||
for i := range qDest {
|
||||
qDest[i] -= quality_shift
|
||||
}
|
||||
seq.TakeQualities(qDest)
|
||||
}
|
||||
|
||||
sequences = append(sequences, seq)
|
||||
}
|
||||
|
||||
return sequences, nil
|
||||
}
|
||||
|
||||
func _ParseFastqFile(
|
||||
input ChannelFileChunk,
|
||||
out obiiter.IBioSequence,
|
||||
@@ -387,14 +313,7 @@ func _ParseFastqFile(
|
||||
parser := FastqChunkParser(quality_shift, with_quality, UtoT)
|
||||
|
||||
for chunks := range input {
|
||||
var sequences obiseq.BioSequenceSlice
|
||||
var err error
|
||||
|
||||
if chunks.Rope != nil {
|
||||
sequences, err = FastqChunkParserRope(chunks.Source, chunks.Rope, quality_shift, with_quality, UtoT)
|
||||
} else {
|
||||
sequences, err = parser(chunks.Source, chunks.Raw)
|
||||
}
|
||||
sequences, err := parser(chunks.Source, chunks.Raw)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("File %s : Cannot parse the fastq file : %v", chunks.Source, err)
|
||||
@@ -420,7 +339,6 @@ func ReadFastq(reader io.Reader, options ...WithOption) (obiiter.IBioSequence, e
|
||||
1024*1024,
|
||||
EndOfLastFastqEntry,
|
||||
"\n@",
|
||||
false,
|
||||
)
|
||||
|
||||
for i := 0; i < nworker; i++ {
|
||||
|
||||
@@ -296,7 +296,7 @@ func _parse_json_header_(header string, sequence *obiseq.BioSequence) string {
|
||||
|
||||
case strings.HasSuffix(skey, "_taxid"):
|
||||
if dataType == jsonparser.Number || dataType == jsonparser.String {
|
||||
rank := skey[:len(skey)-len("_taxid")]
|
||||
rank, _ := obiutils.SplitInTwo(skey, '_')
|
||||
|
||||
taxid := string(value)
|
||||
sequence.SetTaxid(taxid, rank)
|
||||
|
||||
@@ -77,47 +77,45 @@ func FormatFasta(seq *obiseq.BioSequence, formater FormatHeader) string {
|
||||
//
|
||||
// It returns a byte array containing the formatted sequences.
|
||||
func FormatFastaBatch(batch obiiter.BioSequenceBatch, formater FormatHeader, skipEmpty bool) *bytes.Buffer {
|
||||
// Create a buffer to store the formatted sequences
|
||||
var bs bytes.Buffer
|
||||
|
||||
lt := 0
|
||||
|
||||
for _, seq := range batch.Slice() {
|
||||
lt += seq.Len()
|
||||
}
|
||||
|
||||
// Pre-allocate: sequence data + newlines every 60 chars + ~100 bytes header per sequence
|
||||
bs.Grow(lt + lt/60 + 100*batch.Len() + 1)
|
||||
|
||||
// Iterate over each sequence in the batch
|
||||
log.Debugf("FormatFastaBatch: #%d : %d seqs", batch.Order(), batch.Len())
|
||||
|
||||
first := true
|
||||
for _, seq := range batch.Slice() {
|
||||
// Check if the sequence is empty
|
||||
if seq.Len() > 0 {
|
||||
// Write header directly into bs — no intermediate string
|
||||
bs.WriteByte('>')
|
||||
bs.WriteString(seq.Id())
|
||||
bs.WriteByte(' ')
|
||||
bs.WriteString(formater(seq))
|
||||
bs.WriteByte('\n')
|
||||
// Format the sequence using the provided formater function
|
||||
formattedSeq := FormatFasta(seq, formater)
|
||||
|
||||
// Write folded sequence directly into bs — no copies
|
||||
s := seq.Sequence()
|
||||
l := len(s)
|
||||
for i := 0; i < l; i += 60 {
|
||||
to := i + 60
|
||||
if to > l {
|
||||
to = l
|
||||
}
|
||||
bs.Write(s[i:to])
|
||||
bs.WriteByte('\n')
|
||||
if first {
|
||||
bs.Grow(lt + (len(formattedSeq)-seq.Len())*batch.Len()*5/4)
|
||||
first = false
|
||||
}
|
||||
|
||||
// Append the formatted sequence to the buffer
|
||||
bs.WriteString(formattedSeq)
|
||||
bs.WriteByte('\n')
|
||||
} else {
|
||||
// Handle empty sequences
|
||||
if skipEmpty {
|
||||
// Skip empty sequences if skipEmpty is true
|
||||
obilog.Warnf("Sequence %s is empty and skipped in output", seq.Id())
|
||||
} else {
|
||||
// Terminate the program if skipEmpty is false
|
||||
log.Fatalf("Sequence %s is empty", seq.Id())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the byte array representation of the buffer
|
||||
return &bs
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ type SeqFileChunkParser func(string, io.Reader) (obiseq.BioSequenceSlice, error)
|
||||
type FileChunk struct {
|
||||
Source string
|
||||
Raw *bytes.Buffer
|
||||
Rope *PieceOfChunk
|
||||
Order int
|
||||
}
|
||||
|
||||
@@ -98,17 +97,11 @@ func (piece *PieceOfChunk) IsLast() bool {
|
||||
return piece.next == nil
|
||||
}
|
||||
|
||||
func (piece *PieceOfChunk) FileChunk(source string, order int, pack bool) FileChunk {
|
||||
piece = piece.Head()
|
||||
var raw *bytes.Buffer
|
||||
if pack {
|
||||
piece.Pack()
|
||||
raw = bytes.NewBuffer(piece.data)
|
||||
}
|
||||
func (piece *PieceOfChunk) FileChunk(source string, order int) FileChunk {
|
||||
piece.Pack()
|
||||
return FileChunk{
|
||||
Source: source,
|
||||
Raw: raw,
|
||||
Rope: piece,
|
||||
Raw: bytes.NewBuffer(piece.data),
|
||||
Order: order,
|
||||
}
|
||||
}
|
||||
@@ -140,8 +133,7 @@ func ReadFileChunk(
|
||||
reader io.Reader,
|
||||
fileChunkSize int,
|
||||
splitter LastSeqRecord,
|
||||
probe string,
|
||||
pack bool) ChannelFileChunk {
|
||||
probe string) ChannelFileChunk {
|
||||
|
||||
chunk_channel := make(ChannelFileChunk)
|
||||
|
||||
@@ -213,7 +205,7 @@ func ReadFileChunk(
|
||||
|
||||
if len(pieces.data) > 0 {
|
||||
// obilog.Warnf("chuck %d :Read %d bytes from file %s", i, io.Len(), source)
|
||||
chunk_channel <- pieces.FileChunk(source, i, pack)
|
||||
chunk_channel <- pieces.FileChunk(source, i)
|
||||
i++
|
||||
}
|
||||
|
||||
@@ -230,7 +222,7 @@ func ReadFileChunk(
|
||||
|
||||
// Send the last chunk to the channel
|
||||
if pieces.Len() > 0 {
|
||||
chunk_channel <- pieces.FileChunk(source, i, pack)
|
||||
chunk_channel <- pieces.FileChunk(source, i)
|
||||
}
|
||||
|
||||
// Close the readers channel when the end of the file is reached
|
||||
|
||||
@@ -29,265 +29,6 @@ const (
|
||||
|
||||
var _seqlenght_rx = regexp.MustCompile(" +([0-9]+) bp")
|
||||
|
||||
// extractSequence scans the ORIGIN section byte-by-byte directly on the rope,
|
||||
// appending compacted bases to dest. Returns the extended slice.
|
||||
// Stops and returns when "//" is found at the start of a line.
|
||||
// The scanner is left positioned after the "//" line.
|
||||
func (s *ropeScanner) extractSequence(dest []byte, UtoT bool) []byte {
|
||||
lineStart := true
|
||||
skipDigits := true
|
||||
|
||||
for s.current != nil {
|
||||
data := s.current.data[s.pos:]
|
||||
for i, b := range data {
|
||||
if lineStart {
|
||||
if b == '/' {
|
||||
// End-of-record marker "//"
|
||||
s.pos += i + 1
|
||||
if s.pos >= len(s.current.data) {
|
||||
s.current = s.current.Next()
|
||||
s.pos = 0
|
||||
}
|
||||
s.skipToNewline()
|
||||
return dest
|
||||
}
|
||||
lineStart = false
|
||||
skipDigits = true
|
||||
}
|
||||
switch {
|
||||
case b == '\n':
|
||||
lineStart = true
|
||||
case b == '\r':
|
||||
// skip
|
||||
case skipDigits:
|
||||
if b != ' ' && (b < '0' || b > '9') {
|
||||
skipDigits = false
|
||||
if UtoT && b == 'u' {
|
||||
b = 't'
|
||||
}
|
||||
dest = append(dest, b)
|
||||
}
|
||||
case b != ' ':
|
||||
if UtoT && b == 'u' {
|
||||
b = 't'
|
||||
}
|
||||
dest = append(dest, b)
|
||||
}
|
||||
}
|
||||
s.current = s.current.Next()
|
||||
s.pos = 0
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// parseLseqFromLocus extracts the declared sequence length from a LOCUS line.
|
||||
// Format: "LOCUS <id> <length> bp ..."
|
||||
// Returns -1 if not found or parse error.
|
||||
func parseLseqFromLocus(line []byte) int {
|
||||
if len(line) < 13 {
|
||||
return -1
|
||||
}
|
||||
i := 12
|
||||
for i < len(line) && line[i] != ' ' {
|
||||
i++
|
||||
}
|
||||
for i < len(line) && line[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
start := i
|
||||
for i < len(line) && line[i] >= '0' && line[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
if i == start {
|
||||
return -1
|
||||
}
|
||||
n, err := strconv.Atoi(string(line[start:i]))
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Prefix constants for GenBank section headers (byte slices for zero-alloc comparison).
|
||||
var (
|
||||
gbPfxLocus = []byte("LOCUS ")
|
||||
gbPfxDefinition = []byte("DEFINITION ")
|
||||
gbPfxContinue = []byte(" ")
|
||||
gbPfxSource = []byte("SOURCE ")
|
||||
gbPfxFeatures = []byte("FEATURES ")
|
||||
gbPfxOrigin = []byte("ORIGIN")
|
||||
gbPfxContig = []byte("CONTIG")
|
||||
gbPfxEnd = []byte("//")
|
||||
gbPfxDbXref = []byte(` /db_xref="taxon:`)
|
||||
)
|
||||
|
||||
// GenbankChunkParserRope parses a GenBank FileChunk directly from the rope
|
||||
// (PieceOfChunk linked list) without calling Pack(). This eliminates the large
|
||||
// contiguous allocation required for chromosomal-scale sequences.
|
||||
func GenbankChunkParserRope(source string, rope *PieceOfChunk,
|
||||
withFeatureTable, UtoT bool) (obiseq.BioSequenceSlice, error) {
|
||||
|
||||
state := inHeader
|
||||
scanner := newRopeScanner(rope)
|
||||
sequences := obiseq.MakeBioSequenceSlice(100)[:0]
|
||||
|
||||
id := ""
|
||||
lseq := -1
|
||||
scientificName := ""
|
||||
defBytes := new(bytes.Buffer)
|
||||
featBytes := new(bytes.Buffer)
|
||||
var seqDest []byte
|
||||
taxid := 1
|
||||
nl := 0
|
||||
|
||||
for bline := scanner.ReadLine(); bline != nil; bline = scanner.ReadLine() {
|
||||
nl++
|
||||
processed := false
|
||||
for !processed {
|
||||
switch {
|
||||
|
||||
case bytes.HasPrefix(bline, gbPfxLocus):
|
||||
if state != inHeader {
|
||||
log.Fatalf("Line %d - Unexpected state %d while reading LOCUS: %s", nl, state, bline)
|
||||
}
|
||||
rest := bline[12:]
|
||||
sp := bytes.IndexByte(rest, ' ')
|
||||
if sp < 0 {
|
||||
id = string(rest)
|
||||
} else {
|
||||
id = string(rest[:sp])
|
||||
}
|
||||
lseq = parseLseqFromLocus(bline)
|
||||
cap0 := lseq + 20
|
||||
if cap0 < 1024 {
|
||||
cap0 = 1024
|
||||
}
|
||||
seqDest = make([]byte, 0, cap0)
|
||||
state = inEntry
|
||||
processed = true
|
||||
|
||||
case bytes.HasPrefix(bline, gbPfxDefinition):
|
||||
if state != inEntry {
|
||||
log.Fatalf("Line %d - Unexpected state %d while reading DEFINITION: %s", nl, state, bline)
|
||||
}
|
||||
defBytes.Write(bytes.TrimSpace(bline[12:]))
|
||||
state = inDefinition
|
||||
processed = true
|
||||
|
||||
case state == inDefinition:
|
||||
if bytes.HasPrefix(bline, gbPfxContinue) {
|
||||
defBytes.WriteByte(' ')
|
||||
defBytes.Write(bytes.TrimSpace(bline[12:]))
|
||||
processed = true
|
||||
} else {
|
||||
state = inEntry
|
||||
}
|
||||
|
||||
case bytes.HasPrefix(bline, gbPfxSource):
|
||||
if state != inEntry {
|
||||
log.Fatalf("Line %d - Unexpected state %d while reading SOURCE: %s", nl, state, bline)
|
||||
}
|
||||
scientificName = string(bytes.TrimSpace(bline[12:]))
|
||||
processed = true
|
||||
|
||||
case bytes.HasPrefix(bline, gbPfxFeatures):
|
||||
if state != inEntry {
|
||||
log.Fatalf("Line %d - Unexpected state %d while reading FEATURES: %s", nl, state, bline)
|
||||
}
|
||||
if withFeatureTable {
|
||||
featBytes.Write(bline)
|
||||
}
|
||||
state = inFeature
|
||||
processed = true
|
||||
|
||||
case bytes.HasPrefix(bline, gbPfxOrigin):
|
||||
if state != inFeature && state != inContig {
|
||||
log.Fatalf("Line %d - Unexpected state %d while reading ORIGIN: %s", nl, state, bline)
|
||||
}
|
||||
// Use fast byte-scan to extract sequence and consume through "//"
|
||||
seqDest = scanner.extractSequence(seqDest, UtoT)
|
||||
// Emit record
|
||||
if id == "" {
|
||||
log.Warn("Empty id when parsing genbank file")
|
||||
}
|
||||
sequence := obiseq.NewBioSequenceOwning(id, seqDest, defBytes.String())
|
||||
sequence.SetSource(source)
|
||||
if withFeatureTable {
|
||||
sequence.SetFeatures(featBytes.Bytes())
|
||||
}
|
||||
annot := sequence.Annotations()
|
||||
annot["scientific_name"] = scientificName
|
||||
annot["taxid"] = taxid
|
||||
sequences = append(sequences, sequence)
|
||||
|
||||
defBytes = bytes.NewBuffer(obiseq.GetSlice(200))
|
||||
featBytes = new(bytes.Buffer)
|
||||
nl = 0
|
||||
taxid = 1
|
||||
seqDest = nil
|
||||
state = inHeader
|
||||
processed = true
|
||||
|
||||
case bytes.HasPrefix(bline, gbPfxContig):
|
||||
if state != inFeature && state != inContig {
|
||||
log.Fatalf("Line %d - Unexpected state %d while reading CONTIG: %s", nl, state, bline)
|
||||
}
|
||||
state = inContig
|
||||
processed = true
|
||||
|
||||
case bytes.Equal(bline, gbPfxEnd):
|
||||
// Reached for CONTIG records (no ORIGIN section)
|
||||
if state != inContig {
|
||||
log.Fatalf("Line %d - Unexpected state %d while reading end of record %s", nl, state, id)
|
||||
}
|
||||
if id == "" {
|
||||
log.Warn("Empty id when parsing genbank file")
|
||||
}
|
||||
sequence := obiseq.NewBioSequenceOwning(id, seqDest, defBytes.String())
|
||||
sequence.SetSource(source)
|
||||
if withFeatureTable {
|
||||
sequence.SetFeatures(featBytes.Bytes())
|
||||
}
|
||||
annot := sequence.Annotations()
|
||||
annot["scientific_name"] = scientificName
|
||||
annot["taxid"] = taxid
|
||||
sequences = append(sequences, sequence)
|
||||
|
||||
defBytes = bytes.NewBuffer(obiseq.GetSlice(200))
|
||||
featBytes = new(bytes.Buffer)
|
||||
nl = 0
|
||||
taxid = 1
|
||||
seqDest = nil
|
||||
state = inHeader
|
||||
processed = true
|
||||
|
||||
default:
|
||||
switch state {
|
||||
case inFeature:
|
||||
if withFeatureTable {
|
||||
featBytes.WriteByte('\n')
|
||||
featBytes.Write(bline)
|
||||
}
|
||||
if bytes.HasPrefix(bline, gbPfxDbXref) {
|
||||
rest := bline[len(gbPfxDbXref):]
|
||||
q := bytes.IndexByte(rest, '"')
|
||||
if q >= 0 {
|
||||
taxid, _ = strconv.Atoi(string(rest[:q]))
|
||||
}
|
||||
}
|
||||
processed = true
|
||||
case inHeader, inEntry, inContig:
|
||||
processed = true
|
||||
default:
|
||||
log.Fatalf("Unexpected state %d while reading: %s", state, bline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sequences, nil
|
||||
}
|
||||
|
||||
func GenbankChunkParser(withFeatureTable, UtoT bool) func(string, io.Reader) (obiseq.BioSequenceSlice, error) {
|
||||
return func(source string, input io.Reader) (obiseq.BioSequenceSlice, error) {
|
||||
state := inHeader
|
||||
@@ -384,10 +125,13 @@ func GenbankChunkParser(withFeatureTable, UtoT bool) func(string, io.Reader) (ob
|
||||
if state != inSequence && state != inContig {
|
||||
log.Fatalf("Line %d - Unexpected state %d while reading end of record %s", nl, state, id)
|
||||
}
|
||||
// log.Debugln("Total lines := ", nl)
|
||||
if id == "" {
|
||||
log.Warn("Empty id when parsing genbank file")
|
||||
}
|
||||
|
||||
// log.Debugf("End of sequence %s: %dbp ", id, seqBytes.Len())
|
||||
|
||||
sequence := obiseq.NewBioSequence(id,
|
||||
seqBytes.Bytes(),
|
||||
defBytes.String())
|
||||
@@ -400,6 +144,9 @@ func GenbankChunkParser(withFeatureTable, UtoT bool) func(string, io.Reader) (ob
|
||||
annot := sequence.Annotations()
|
||||
annot["scientific_name"] = scientificName
|
||||
annot["taxid"] = taxid
|
||||
// log.Println(FormatFasta(sequence, FormatFastSeqJsonHeader))
|
||||
// log.Debugf("Read sequences %s: %dbp (%d)", sequence.Id(),
|
||||
// sequence.Len(), seqBytes.Len())
|
||||
|
||||
sequences = append(sequences, sequence)
|
||||
|
||||
@@ -412,6 +159,8 @@ func GenbankChunkParser(withFeatureTable, UtoT bool) func(string, io.Reader) (ob
|
||||
processed = true
|
||||
|
||||
case state == inSequence:
|
||||
// log.Debugf("Chunk %d : Genbank: line %d, state = %d : %s", chunks.order, nl, state, line)
|
||||
|
||||
sl++
|
||||
cleanline := strings.TrimSpace(line)
|
||||
parts := strings.SplitN(cleanline, " ", 7)
|
||||
@@ -449,7 +198,6 @@ func GenbankChunkParser(withFeatureTable, UtoT bool) func(string, io.Reader) (ob
|
||||
|
||||
}
|
||||
|
||||
_ = sl
|
||||
return sequences, nil
|
||||
}
|
||||
}
|
||||
@@ -458,16 +206,10 @@ func _ParseGenbankFile(input ChannelFileChunk,
|
||||
out obiiter.IBioSequence,
|
||||
withFeatureTable, UtoT bool) {
|
||||
|
||||
for chunks := range input {
|
||||
var sequences obiseq.BioSequenceSlice
|
||||
var err error
|
||||
parser := GenbankChunkParser(withFeatureTable, UtoT)
|
||||
|
||||
if chunks.Rope != nil {
|
||||
sequences, err = GenbankChunkParserRope(chunks.Source, chunks.Rope, withFeatureTable, UtoT)
|
||||
} else {
|
||||
parser := GenbankChunkParser(withFeatureTable, UtoT)
|
||||
sequences, err = parser(chunks.Source, chunks.Raw)
|
||||
}
|
||||
for chunks := range input {
|
||||
sequences, err := parser(chunks.Source, chunks.Raw)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("File %s : Cannot parse the genbank file : %v", chunks.Source, err)
|
||||
@@ -483,6 +225,7 @@ func _ParseGenbankFile(input ChannelFileChunk,
|
||||
|
||||
func ReadGenbank(reader io.Reader, options ...WithOption) (obiiter.IBioSequence, error) {
|
||||
opt := MakeOptions(options)
|
||||
// entry_channel := make(chan _FileChunk)
|
||||
|
||||
entry_channel := ReadFileChunk(
|
||||
opt.Source(),
|
||||
@@ -490,13 +233,13 @@ func ReadGenbank(reader io.Reader, options ...WithOption) (obiiter.IBioSequence,
|
||||
1024*1024*128,
|
||||
EndOfLastFlatFileEntry,
|
||||
"\nLOCUS ",
|
||||
false, // do not pack: rope-based parser avoids contiguous allocation
|
||||
)
|
||||
|
||||
newIter := obiiter.MakeIBioSequence()
|
||||
|
||||
nworkers := opt.ParallelWorkers()
|
||||
|
||||
// for j := 0; j < opt.ParallelWorkers(); j++ {
|
||||
for j := 0; j < nworkers; j++ {
|
||||
newIter.Add(1)
|
||||
go _ParseGenbankFile(
|
||||
@@ -507,6 +250,8 @@ func ReadGenbank(reader io.Reader, options ...WithOption) (obiiter.IBioSequence,
|
||||
)
|
||||
}
|
||||
|
||||
// go _ReadFlatFileChunk(reader, entry_channel)
|
||||
|
||||
go func() {
|
||||
newIter.WaitAndClose()
|
||||
log.Debug("End of the genbank file ", opt.Source())
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package obiformats
|
||||
|
||||
import "bytes"
|
||||
|
||||
// ropeScanner reads lines from a PieceOfChunk rope.
|
||||
// The carry buffer handles lines that span two rope nodes; it grows as needed.
|
||||
type ropeScanner struct {
|
||||
current *PieceOfChunk
|
||||
pos int
|
||||
carry []byte
|
||||
}
|
||||
|
||||
func newRopeScanner(rope *PieceOfChunk) *ropeScanner {
|
||||
return &ropeScanner{current: rope}
|
||||
}
|
||||
|
||||
// ReadLine returns the next line without the trailing \n (or \r\n).
|
||||
// Returns nil at end of rope. The returned slice aliases carry[] or the node
|
||||
// data and is valid only until the next ReadLine call.
|
||||
func (s *ropeScanner) ReadLine() []byte {
|
||||
for {
|
||||
if s.current == nil {
|
||||
if len(s.carry) > 0 {
|
||||
line := s.carry
|
||||
s.carry = s.carry[:0]
|
||||
return line
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
data := s.current.data[s.pos:]
|
||||
idx := bytes.IndexByte(data, '\n')
|
||||
|
||||
if idx >= 0 {
|
||||
var line []byte
|
||||
if len(s.carry) == 0 {
|
||||
line = data[:idx]
|
||||
} else {
|
||||
s.carry = append(s.carry, data[:idx]...)
|
||||
line = s.carry
|
||||
s.carry = s.carry[:0]
|
||||
}
|
||||
s.pos += idx + 1
|
||||
if s.pos >= len(s.current.data) {
|
||||
s.current = s.current.Next()
|
||||
s.pos = 0
|
||||
}
|
||||
if len(line) > 0 && line[len(line)-1] == '\r' {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// No \n in this node: accumulate into carry and advance
|
||||
s.carry = append(s.carry, data...)
|
||||
s.current = s.current.Next()
|
||||
s.pos = 0
|
||||
}
|
||||
}
|
||||
|
||||
// skipToNewline advances the scanner past the next '\n'.
|
||||
func (s *ropeScanner) skipToNewline() {
|
||||
for s.current != nil {
|
||||
data := s.current.data[s.pos:]
|
||||
idx := bytes.IndexByte(data, '\n')
|
||||
if idx >= 0 {
|
||||
s.pos += idx + 1
|
||||
if s.pos >= len(s.current.data) {
|
||||
s.current = s.current.Next()
|
||||
s.pos = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
s.current = s.current.Next()
|
||||
s.pos = 0
|
||||
}
|
||||
}
|
||||
@@ -444,67 +444,6 @@ func (iterator IBioSequence) Rebatch(size int) IBioSequence {
|
||||
return newIter
|
||||
}
|
||||
|
||||
// RebatchBySize reorganises the stream into batches bounded by two independent
|
||||
// upper limits: maxCount (max number of sequences) and maxBytes (max cumulative
|
||||
// estimated memory). A batch is flushed as soon as either limit would be
|
||||
// exceeded. A single sequence larger than maxBytes is always emitted alone.
|
||||
// Passing 0 for a limit disables that constraint; if both are 0 it falls back
|
||||
// to Rebatch(obidefault.BatchSizeMax()).
|
||||
func (iterator IBioSequence) RebatchBySize(maxBytes int, maxCount int) IBioSequence {
|
||||
if maxBytes <= 0 && maxCount <= 0 {
|
||||
return iterator.Rebatch(obidefault.BatchSizeMax())
|
||||
}
|
||||
|
||||
newIter := MakeIBioSequence()
|
||||
|
||||
newIter.Add(1)
|
||||
|
||||
go func() {
|
||||
newIter.WaitAndClose()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
order := 0
|
||||
iterator = iterator.SortBatches()
|
||||
buffer := obiseq.MakeBioSequenceSlice()
|
||||
bufBytes := 0
|
||||
source := ""
|
||||
|
||||
flush := func() {
|
||||
if len(buffer) > 0 {
|
||||
newIter.Push(MakeBioSequenceBatch(source, order, buffer))
|
||||
order++
|
||||
buffer = obiseq.MakeBioSequenceSlice()
|
||||
bufBytes = 0
|
||||
}
|
||||
}
|
||||
|
||||
for iterator.Next() {
|
||||
seqs := iterator.Get()
|
||||
source = seqs.Source()
|
||||
for _, s := range seqs.Slice() {
|
||||
sz := s.MemorySize()
|
||||
countFull := maxCount > 0 && len(buffer) >= maxCount
|
||||
memFull := maxBytes > 0 && bufBytes+sz > maxBytes && len(buffer) > 0
|
||||
if countFull || memFull {
|
||||
flush()
|
||||
}
|
||||
buffer = append(buffer, s)
|
||||
bufBytes += sz
|
||||
}
|
||||
}
|
||||
flush()
|
||||
|
||||
newIter.Done()
|
||||
}()
|
||||
|
||||
if iterator.IsPaired() {
|
||||
newIter.MarkAsPaired()
|
||||
}
|
||||
|
||||
return newIter
|
||||
}
|
||||
|
||||
func (iterator IBioSequence) FilterEmpty() IBioSequence {
|
||||
|
||||
newIter := MakeIBioSequence()
|
||||
@@ -699,7 +638,7 @@ func (iterator IBioSequence) FilterOn(predicate obiseq.SequencePredicate,
|
||||
trueIter.MarkAsPaired()
|
||||
}
|
||||
|
||||
return trueIter.RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax())
|
||||
return trueIter.Rebatch(size)
|
||||
}
|
||||
|
||||
func (iterator IBioSequence) FilterAnd(predicate obiseq.SequencePredicate,
|
||||
@@ -755,7 +694,7 @@ func (iterator IBioSequence) FilterAnd(predicate obiseq.SequencePredicate,
|
||||
trueIter.MarkAsPaired()
|
||||
}
|
||||
|
||||
return trueIter.RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax())
|
||||
return trueIter.Rebatch(size)
|
||||
}
|
||||
|
||||
// Load all sequences availables from an IBioSequenceBatch iterator into
|
||||
|
||||
@@ -3,7 +3,6 @@ package obiiter
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiseq"
|
||||
)
|
||||
|
||||
@@ -71,7 +70,7 @@ func IFragments(minsize, length, overlap, size, nworkers int) Pipeable {
|
||||
}
|
||||
go f(iterator)
|
||||
|
||||
return newiter.SortBatches().RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax())
|
||||
return newiter.SortBatches().Rebatch(size)
|
||||
}
|
||||
|
||||
return ifrg
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obidefault"
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiformats"
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/DavidGamba/go-getoptions"
|
||||
@@ -56,15 +55,7 @@ func RegisterGlobalOptions(options *getoptions.GetOpt) {
|
||||
|
||||
options.IntVar(obidefault.BatchSizePtr(), "batch-size", obidefault.BatchSize(),
|
||||
options.GetEnv("OBIBATCHSIZE"),
|
||||
options.Description("Minimum number of sequences per batch (floor, default 1)"))
|
||||
|
||||
options.IntVar(obidefault.BatchSizeMaxPtr(), "batch-size-max", obidefault.BatchSizeMax(),
|
||||
options.GetEnv("OBIBATCHSIZEMAX"),
|
||||
options.Description("Maximum number of sequences per batch (ceiling, default 2000)"))
|
||||
|
||||
options.StringVar(obidefault.BatchMemStrPtr(), "batch-mem", "",
|
||||
options.GetEnv("OBIBATCHMEM"),
|
||||
options.Description("Maximum memory per batch (e.g. 128K, 64M, 1G; default: 128M). Set to 0 to disable."))
|
||||
options.Description("Number of sequence per batch for paralelle processing"))
|
||||
|
||||
options.Bool("solexa", false,
|
||||
options.GetEnv("OBISOLEXA"),
|
||||
@@ -166,15 +157,6 @@ func ProcessParsedOptions(options *getoptions.GetOpt, parseErr error) {
|
||||
if options.Called("solexa") {
|
||||
obidefault.SetReadQualitiesShift(64)
|
||||
}
|
||||
|
||||
if options.Called("batch-mem") {
|
||||
n, err := obiutils.ParseMemSize(obidefault.BatchMemStr())
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid --batch-mem value %q: %v", obidefault.BatchMemStr(), err)
|
||||
}
|
||||
obidefault.SetBatchMem(n)
|
||||
log.Printf("Memory-based batching enabled: %s per batch", obidefault.BatchMemStr())
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateOptionParser(program string,
|
||||
|
||||
@@ -3,7 +3,7 @@ package obioptions
|
||||
// Version is automatically updated by the Makefile from version.txt
|
||||
// The patch number (third digit) is incremented on each push to the repository
|
||||
|
||||
var _Version = "Release 4.4.24"
|
||||
var _Version = "Release 4.4.16"
|
||||
|
||||
// Version returns the version of the obitools package.
|
||||
//
|
||||
|
||||
@@ -120,19 +120,6 @@ func NewBioSequence(id string,
|
||||
return bs
|
||||
}
|
||||
|
||||
// NewBioSequenceOwning creates a BioSequence taking ownership of the sequence
|
||||
// slice without copying it. The caller must not use the slice after this call.
|
||||
// Use this when the slice was allocated specifically for this sequence.
|
||||
func NewBioSequenceOwning(id string,
|
||||
sequence []byte,
|
||||
definition string) *BioSequence {
|
||||
bs := NewEmptyBioSequence(0)
|
||||
bs.SetId(id)
|
||||
bs.TakeSequence(sequence)
|
||||
bs.SetDefinition(definition)
|
||||
return bs
|
||||
}
|
||||
|
||||
// NewBioSequenceWithQualities creates a new BioSequence object with the given id, sequence, definition, and qualities.
|
||||
//
|
||||
// Parameters:
|
||||
@@ -273,28 +260,6 @@ func (s *BioSequence) Len() int {
|
||||
return len(s.sequence)
|
||||
}
|
||||
|
||||
// MemorySize returns an estimate of the memory footprint of the BioSequence
|
||||
// in bytes. It accounts for the sequence, quality scores, feature data,
|
||||
// annotations, and fixed struct overhead. The estimate is conservative
|
||||
// (cap rather than len for byte slices) so it is suitable for memory-based
|
||||
// batching decisions.
|
||||
func (s *BioSequence) MemorySize() int {
|
||||
if s == nil {
|
||||
return 0
|
||||
}
|
||||
// fixed struct overhead (strings, pointers, mutex pointer)
|
||||
const overhead = 128
|
||||
n := overhead
|
||||
n += cap(s.sequence)
|
||||
n += cap(s.qualities)
|
||||
n += cap(s.feature)
|
||||
n += len(s.id)
|
||||
n += len(s.source)
|
||||
// rough annotation estimate: each key+value pair ~64 bytes on average
|
||||
n += len(s.annotations) * 64
|
||||
return n
|
||||
}
|
||||
|
||||
// HasQualities checks if the BioSequence has sequence qualitiy scores.
|
||||
//
|
||||
// This function does not have any parameters.
|
||||
@@ -479,12 +444,6 @@ func (s *BioSequence) SetSequence(sequence []byte) {
|
||||
s.sequence = obiutils.InPlaceToLower(CopySlice(sequence))
|
||||
}
|
||||
|
||||
// TakeSequence stores the slice directly without copying, then lowercases in-place.
|
||||
// The caller must not use the slice after this call.
|
||||
func (s *BioSequence) TakeSequence(sequence []byte) {
|
||||
s.sequence = obiutils.InPlaceToLower(sequence)
|
||||
}
|
||||
|
||||
func (s *BioSequence) HasValidSequence() bool {
|
||||
for _, c := range s.sequence {
|
||||
if !((c >= 'a' && c <= 'z') || c == '-' || c == '.' || c == '[' || c == ']') {
|
||||
@@ -502,15 +461,6 @@ func (s *BioSequence) SetQualities(qualities Quality) {
|
||||
s.qualities = CopySlice(qualities)
|
||||
}
|
||||
|
||||
// TakeQualities stores the slice directly without copying.
|
||||
// The caller must not use the slice after this call.
|
||||
func (s *BioSequence) TakeQualities(qualities Quality) {
|
||||
if s.qualities != nil {
|
||||
RecycleSlice(&s.qualities)
|
||||
}
|
||||
s.qualities = qualities
|
||||
}
|
||||
|
||||
// A method that appends a byte slice to the qualities of the BioSequence.
|
||||
func (s *BioSequence) WriteQualities(data []byte) (int, error) {
|
||||
s.qualities = append(s.qualities, data...)
|
||||
|
||||
@@ -195,7 +195,7 @@ func (s *BioSequenceSlice) ExtractTaxonomy(taxonomy *obitax.Taxonomy, seqAsTaxa
|
||||
return nil, fmt.Errorf("sequence %v has no path", s.Id())
|
||||
}
|
||||
last := path[len(path)-1]
|
||||
taxname, _ := obiutils.LeftSplitInTwo(last, ':')
|
||||
taxname, _ := obiutils.SplitInTwo(last, ':')
|
||||
if idx, ok := s.GetIntAttribute("seq_number"); !ok {
|
||||
return nil, errors.New("sequences are not numbered")
|
||||
} else {
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
package obiseq
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"git.metabarcoding.org/obitools/obitools4/obitools4/pkg/obiutils"
|
||||
)
|
||||
|
||||
const _LargeSliceThreshold = 100 * 1024 // 100 kb — below: leave to GC, above: trigger explicit GC
|
||||
const _GCBytesBudget = int64(256 * 1024 * 1024) // trigger GC every 256 MB of large discards
|
||||
|
||||
var _largeSliceDiscardedBytes = atomic.Int64{}
|
||||
|
||||
var _BioSequenceByteSlicePool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
bs := make([]byte, 0, 300)
|
||||
@@ -41,13 +34,6 @@ func RecycleSlice(s *[]byte) {
|
||||
}
|
||||
if cap(*s) <= 1024 {
|
||||
_BioSequenceByteSlicePool.Put(s)
|
||||
} else if cap(*s) >= _LargeSliceThreshold {
|
||||
n := int64(cap(*s))
|
||||
*s = nil
|
||||
prev := _largeSliceDiscardedBytes.Load()
|
||||
if _largeSliceDiscardedBytes.Add(n)/_GCBytesBudget > prev/_GCBytesBudget {
|
||||
runtime.GC()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func NewTaxidFactory(code string, alphabet obiutils.AsciiSet) *TaxidFactory {
|
||||
// It extracts the relevant part of the string after the first colon (':') if present.
|
||||
func (f *TaxidFactory) FromString(taxid string) (Taxid, error) {
|
||||
taxid = obiutils.AsciiSpaceSet.TrimLeft(taxid)
|
||||
part1, part2 := obiutils.LeftSplitInTwo(taxid, ':')
|
||||
part1, part2 := obiutils.SplitInTwo(taxid, ':')
|
||||
if len(part2) == 0 {
|
||||
taxid = part1
|
||||
} else {
|
||||
|
||||
@@ -64,7 +64,7 @@ func EmpiricalDistCsv(filename string, data [][]Ratio, compressed bool) {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
destfile, err := obiutils.CompressStream(file, compressed, true)
|
||||
destfile, err := obiutils.CompressStream(file, true, true)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
@@ -214,8 +214,6 @@ func CLIReadBioSequences(filenames ...string) (obiiter.IBioSequence, error) {
|
||||
|
||||
iterator = iterator.Speed("Reading sequences")
|
||||
|
||||
iterator = iterator.RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax())
|
||||
|
||||
return iterator, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -291,5 +291,5 @@ func IndexReferenceDB(iterator obiiter.IBioSequence) obiiter.IBioSequence {
|
||||
go f()
|
||||
}
|
||||
|
||||
return indexed.RebatchBySize(obidefault.BatchMem(), obidefault.BatchSizeMax())
|
||||
return indexed.Rebatch(obidefault.BatchSize())
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
package obiutils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// ParseMemSize parses a human-readable memory size string and returns the
|
||||
// equivalent number of bytes. The value is a number optionally followed by a
|
||||
// unit suffix (case-insensitive):
|
||||
//
|
||||
// B or (no suffix) — bytes
|
||||
// K or KB — kibibytes (1 024)
|
||||
// M or MB — mebibytes (1 048 576)
|
||||
// G or GB — gibibytes (1 073 741 824)
|
||||
// T or TB — tebibytes (1 099 511 627 776)
|
||||
//
|
||||
// Examples: "512", "128K", "128k", "64M", "1G", "2GB"
|
||||
func ParseMemSize(s string) (int, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0, fmt.Errorf("empty memory size string")
|
||||
}
|
||||
|
||||
// split numeric prefix from unit suffix
|
||||
i := 0
|
||||
for i < len(s) && (unicode.IsDigit(rune(s[i])) || s[i] == '.') {
|
||||
i++
|
||||
}
|
||||
numStr := s[:i]
|
||||
unit := strings.ToUpper(strings.TrimSpace(s[i:]))
|
||||
// strip trailing 'B' from two-letter units (KB→K, MB→M …)
|
||||
if len(unit) == 2 && unit[1] == 'B' {
|
||||
unit = unit[:1]
|
||||
}
|
||||
|
||||
val, err := strconv.ParseFloat(numStr, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid memory size %q: %w", s, err)
|
||||
}
|
||||
|
||||
var multiplier float64
|
||||
switch unit {
|
||||
case "", "B":
|
||||
multiplier = 1
|
||||
case "K":
|
||||
multiplier = 1024
|
||||
case "M":
|
||||
multiplier = 1024 * 1024
|
||||
case "G":
|
||||
multiplier = 1024 * 1024 * 1024
|
||||
case "T":
|
||||
multiplier = 1024 * 1024 * 1024 * 1024
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown memory unit %q in %q", unit, s)
|
||||
}
|
||||
|
||||
return int(val * multiplier), nil
|
||||
}
|
||||
|
||||
// FormatMemSize formats a byte count as a human-readable string with the
|
||||
// largest unit that produces a value ≥ 1 (e.g. 1536 → "1.5K").
|
||||
func FormatMemSize(n int) string {
|
||||
units := []struct {
|
||||
suffix string
|
||||
size int
|
||||
}{
|
||||
{"T", 1024 * 1024 * 1024 * 1024},
|
||||
{"G", 1024 * 1024 * 1024},
|
||||
{"M", 1024 * 1024},
|
||||
{"K", 1024},
|
||||
}
|
||||
for _, u := range units {
|
||||
if n >= u.size {
|
||||
v := float64(n) / float64(u.size)
|
||||
if v == float64(int(v)) {
|
||||
return fmt.Sprintf("%d%s", int(v), u.suffix)
|
||||
}
|
||||
return fmt.Sprintf("%.1f%s", v, u.suffix)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%dB", n)
|
||||
}
|
||||
@@ -144,7 +144,7 @@ func (r *AsciiSet) TrimLeft(s string) string {
|
||||
return s[i:]
|
||||
}
|
||||
|
||||
func LeftSplitInTwo(s string, sep byte) (string, string) {
|
||||
func SplitInTwo(s string, sep byte) (string, string) {
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
@@ -157,17 +157,3 @@ func LeftSplitInTwo(s string, sep byte) (string, string) {
|
||||
}
|
||||
return s[:i], s[i+1:]
|
||||
}
|
||||
|
||||
func RightSplitInTwo(s string, sep byte) (string, string) {
|
||||
i := len(s) - 1
|
||||
for ; i >= 0; i-- {
|
||||
c := s[i]
|
||||
if c == sep {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i == len(s) {
|
||||
return s, ""
|
||||
}
|
||||
return s[:i], s[i+1:]
|
||||
}
|
||||
|
||||
294
release_notes.sh
294
release_notes.sh
@@ -1,294 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Generate GitHub-compatible release notes for an OBITools4 version.
|
||||
#
|
||||
# Usage:
|
||||
# ./release_notes.sh # latest version
|
||||
# ./release_notes.sh -v 4.4.15 # specific version
|
||||
# ./release_notes.sh -l # list available versions
|
||||
# ./release_notes.sh -r # raw commit list (no LLM)
|
||||
# ./release_notes.sh -c -v 4.4.16 # show LLM context for a version
|
||||
|
||||
GITHUB_REPO="metabarcoding/obitools4"
|
||||
GITHUB_API="https://api.github.com/repos/${GITHUB_REPO}"
|
||||
VERSION=""
|
||||
LIST_VERSIONS=false
|
||||
RAW_MODE=false
|
||||
CONTEXT_MODE=false
|
||||
LLM_MODEL="ollama:qwen3-coder-next:latest"
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
die() { echo "Error: $*" >&2; exit 1; }
|
||||
|
||||
next_patch() {
|
||||
local v="$1"
|
||||
local major minor patch
|
||||
major=$(echo "$v" | cut -d. -f1)
|
||||
minor=$(echo "$v" | cut -d. -f2)
|
||||
patch=$(echo "$v" | cut -d. -f3)
|
||||
echo "${major}.${minor}.$(( patch + 1 ))"
|
||||
}
|
||||
|
||||
# Strip "pre-" prefix to get the bare version number for installation section
|
||||
bare_version() {
|
||||
echo "$1" | sed 's/^pre-//'
|
||||
}
|
||||
|
||||
installation_section() {
|
||||
local v
|
||||
v=$(bare_version "$1")
|
||||
cat <<INSTALL_EOF
|
||||
|
||||
## Installation
|
||||
|
||||
### Pre-built binaries
|
||||
|
||||
Download the appropriate archive for your system from the
|
||||
[release assets](https://github.com/metabarcoding/obitools4/releases/tag/Release_${v})
|
||||
and extract it:
|
||||
|
||||
#### Linux (AMD64)
|
||||
\`\`\`bash
|
||||
tar -xzf obitools4_${v}_linux_amd64.tar.gz
|
||||
\`\`\`
|
||||
|
||||
#### Linux (ARM64)
|
||||
\`\`\`bash
|
||||
tar -xzf obitools4_${v}_linux_arm64.tar.gz
|
||||
\`\`\`
|
||||
|
||||
#### macOS (Intel)
|
||||
\`\`\`bash
|
||||
tar -xzf obitools4_${v}_darwin_amd64.tar.gz
|
||||
\`\`\`
|
||||
|
||||
#### macOS (Apple Silicon)
|
||||
\`\`\`bash
|
||||
tar -xzf obitools4_${v}_darwin_arm64.tar.gz
|
||||
\`\`\`
|
||||
|
||||
All OBITools4 binaries are included in each archive.
|
||||
|
||||
### From source
|
||||
|
||||
You can also compile and install OBITools4 directly from source using the
|
||||
installation script:
|
||||
|
||||
\`\`\`bash
|
||||
curl -L https://raw.githubusercontent.com/metabarcoding/obitools4/master/install_obitools.sh | bash -s -- --version ${v}
|
||||
\`\`\`
|
||||
|
||||
By default binaries are installed in \`/usr/local/bin\`. Use \`--install-dir\` to
|
||||
change the destination and \`--obitools-prefix\` to add a prefix to command names:
|
||||
|
||||
\`\`\`bash
|
||||
curl -L https://raw.githubusercontent.com/metabarcoding/obitools4/master/install_obitools.sh | \\
|
||||
bash -s -- --version ${v} --install-dir ~/local --obitools-prefix k
|
||||
\`\`\`
|
||||
INSTALL_EOF
|
||||
}
|
||||
|
||||
display_help() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Generate GitHub-compatible Markdown release notes for an OBITools4 version.
|
||||
|
||||
Options:
|
||||
-v, --version VERSION Target version (e.g., 4.4.15). Default: latest.
|
||||
-l, --list List all available versions and exit.
|
||||
-r, --raw Output raw commit list without LLM summarization.
|
||||
-c, --context Show the exact context (commits + prompt) sent to the LLM.
|
||||
-m, --model MODEL LLM model for orla (default: $LLM_MODEL).
|
||||
-h, --help Display this help message.
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # release notes for the latest version
|
||||
$(basename "$0") -v 4.4.15 # release notes for a specific version
|
||||
$(basename "$0") -l # list versions
|
||||
$(basename "$0") -r -v 4.4.15 # raw commit log for a version
|
||||
$(basename "$0") -c -v 4.4.16 # show LLM context for a version
|
||||
EOF
|
||||
}
|
||||
|
||||
# Fetch all Release tags from GitHub API (sorted newest first)
|
||||
fetch_versions() {
|
||||
curl -sf "${GITHUB_API}/releases" \
|
||||
| grep '"tag_name":' \
|
||||
| sed -E 's/.*"tag_name": "Release_([0-9.]+)".*/\1/' \
|
||||
| sort -V -r
|
||||
}
|
||||
|
||||
# ── Parse arguments ──────────────────────────────────────────────────────
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-v|--version) VERSION="$2"; shift 2 ;;
|
||||
-l|--list) LIST_VERSIONS=true; shift ;;
|
||||
-r|--raw) RAW_MODE=true; shift ;;
|
||||
-c|--context) CONTEXT_MODE=true; shift ;;
|
||||
-m|--model) LLM_MODEL="$2"; shift 2 ;;
|
||||
-h|--help) display_help; exit 0 ;;
|
||||
*) die "Unsupported option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── List mode ────────────────────────────────────────────────────────────
|
||||
|
||||
if [ "$LIST_VERSIONS" = true ]; then
|
||||
echo "Available OBITools4 versions:" >&2
|
||||
echo "==============================" >&2
|
||||
fetch_versions
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Resolve versions ─────────────────────────────────────────────────────
|
||||
|
||||
all_versions=$(fetch_versions)
|
||||
[ -z "$all_versions" ] && die "Could not fetch versions from GitHub"
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
# ── Pre-release mode: local HEAD vs latest GitHub tag ──────────────────
|
||||
PRE_RELEASE=true
|
||||
previous_tag="Release_${latest_version}"
|
||||
VERSION="pre-$(next_patch "$latest_version")"
|
||||
|
||||
echo "Pre-release mode: $previous_tag -> HEAD (as $VERSION)" >&2
|
||||
|
||||
# Need to be in a git repo
|
||||
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
die "Not inside a git repository. Pre-release mode requires a local git repo."
|
||||
fi
|
||||
|
||||
# Check that the previous tag exists locally
|
||||
if ! git rev-parse "$previous_tag" >/dev/null 2>&1; then
|
||||
echo "Tag $previous_tag not found locally, fetching..." >&2
|
||||
git fetch --tags 2>/dev/null || true
|
||||
if ! git rev-parse "$previous_tag" >/dev/null 2>&1; then
|
||||
die "Tag $previous_tag not found locally or remotely"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get local commits from the tag to HEAD (full messages)
|
||||
commit_list=$(git log --format="%h %B" "${previous_tag}..HEAD" 2>/dev/null)
|
||||
|
||||
if [ -z "$commit_list" ]; then
|
||||
die "No local commits found since $previous_tag"
|
||||
fi
|
||||
else
|
||||
# ── Published release mode: between two GitHub tags ────────────────────
|
||||
PRE_RELEASE=false
|
||||
tag_name="Release_${VERSION}"
|
||||
|
||||
# Verify the requested version exists
|
||||
if ! echo "$all_versions" | grep -qx "$VERSION"; then
|
||||
die "Version $VERSION not found. Use -l to list available versions."
|
||||
fi
|
||||
|
||||
# Find the previous version
|
||||
previous_version=$(echo "$all_versions" | grep -A1 -x "$VERSION" | tail -1)
|
||||
|
||||
if [ "$previous_version" = "$VERSION" ] || [ -z "$previous_version" ]; then
|
||||
previous_tag=""
|
||||
echo "No previous version found -- will include all commits for $tag_name" >&2
|
||||
else
|
||||
previous_tag="Release_${previous_version}"
|
||||
echo "Generating notes: $previous_tag -> $tag_name" >&2
|
||||
fi
|
||||
|
||||
# Fetch commit messages between tags via GitHub compare API
|
||||
if [ -n "$previous_tag" ]; then
|
||||
commits_json=$(curl -sf "${GITHUB_API}/compare/${previous_tag}...${tag_name}")
|
||||
if [ -z "$commits_json" ]; then
|
||||
die "Could not fetch commit comparison from GitHub"
|
||||
fi
|
||||
commit_list=$(echo "$commits_json" \
|
||||
| jq -r '.commits[] | (.sha[:8] + " " + .commit.message)' 2>/dev/null)
|
||||
else
|
||||
commits_json=$(curl -sf "${GITHUB_API}/commits?sha=${tag_name}&per_page=50")
|
||||
if [ -z "$commits_json" ]; then
|
||||
die "Could not fetch commits from GitHub"
|
||||
fi
|
||||
commit_list=$(echo "$commits_json" \
|
||||
| jq -r '.[] | (.sha[:8] + " " + .commit.message)' 2>/dev/null)
|
||||
fi
|
||||
|
||||
if [ -z "$commit_list" ]; then
|
||||
die "No commits found between $previous_tag and $tag_name"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── LLM prompt (shared by context mode and summarization) ────────────────
|
||||
|
||||
LLM_PROMPT="Summarize the following commits into a GitHub release note for version ${VERSION}. \
|
||||
Ignore commits related to version bumps, .gitignore changes, or any internal housekeeping \
|
||||
that is irrelevant to end users. Describe each user-facing change precisely without exposing \
|
||||
code. Eliminate redundancy. Output strictly valid JSON with no surrounding text, using this \
|
||||
exact schema: {\"title\": \"<short release title>\", \"body\": \"<detailed markdown release notes>\"}"
|
||||
|
||||
# ── Raw mode: just output the commit list ────────────────────────────────
|
||||
|
||||
if [ "$RAW_MODE" = true ]; then
|
||||
echo "# Release ${VERSION}"
|
||||
echo ""
|
||||
echo "## Commits"
|
||||
echo ""
|
||||
echo "$commit_list" | while IFS= read -r line; do
|
||||
echo "- ${line}"
|
||||
done
|
||||
installation_section "$VERSION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Context mode: show what would be sent to the LLM ────────────────────
|
||||
|
||||
if [ "$CONTEXT_MODE" = true ]; then
|
||||
echo "=== LLM Model ==="
|
||||
echo "$LLM_MODEL"
|
||||
echo ""
|
||||
echo "=== Prompt ==="
|
||||
echo "$LLM_PROMPT"
|
||||
echo ""
|
||||
echo "=== Stdin (commit list) ==="
|
||||
echo "$commit_list"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── LLM summarization ───────────────────────────────────────────────────
|
||||
|
||||
if ! command -v orla >/dev/null 2>&1; then
|
||||
die "orla is required for LLM summarization. Use -r for raw output."
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
die "jq is required for JSON parsing. Use -r for raw output."
|
||||
fi
|
||||
|
||||
echo "Summarizing with LLM ($LLM_MODEL)..." >&2
|
||||
|
||||
raw_output=$(echo "$commit_list" | \
|
||||
ORLA_MAX_TOOL_CALLS=50 orla agent -m "$LLM_MODEL" \
|
||||
"$LLM_PROMPT" \
|
||||
2>/dev/null) || true
|
||||
|
||||
if [ -z "$raw_output" ]; then
|
||||
echo "Warning: LLM returned empty output, falling back to raw mode" >&2
|
||||
exec "$0" -r -v "$VERSION"
|
||||
fi
|
||||
|
||||
# Sanitize: extract JSON object, strip control characters
|
||||
sanitized=$(echo "$raw_output" | sed -n '/^{/,/^}/p' | tr -d '\000-\011\013-\014\016-\037')
|
||||
|
||||
release_title=$(echo "$sanitized" | jq -r '.title // empty' 2>/dev/null)
|
||||
release_body=$(echo "$sanitized" | jq -r '.body // empty' 2>/dev/null)
|
||||
|
||||
if [ -n "$release_title" ] && [ -n "$release_body" ]; then
|
||||
echo "# ${release_title}"
|
||||
echo ""
|
||||
echo "$release_body"
|
||||
installation_section "$VERSION"
|
||||
else
|
||||
echo "Warning: JSON parsing failed, falling back to raw mode" >&2
|
||||
exec "$0" -r -v "$VERSION"
|
||||
fi
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Read potentially malformed JSON from stdin (aichat output), extract title and
|
||||
body, and print them as plain text: title on first line, blank line, then body.
|
||||
Exits with 1 on failure (no output).
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
|
||||
text = sys.stdin.read()
|
||||
|
||||
m = re.search(r'\{.*\}', text, re.DOTALL)
|
||||
if not m:
|
||||
sys.exit(1)
|
||||
|
||||
s = m.group()
|
||||
obj = None
|
||||
|
||||
try:
|
||||
obj = json.loads(s)
|
||||
except Exception:
|
||||
s2 = re.sub(r'(?<!\\)\n', r'\\n', s)
|
||||
try:
|
||||
obj = json.loads(s2)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
|
||||
title = obj.get('title', '').strip()
|
||||
body = obj.get('body', '').strip()
|
||||
|
||||
if not title or not body:
|
||||
sys.exit(1)
|
||||
|
||||
print(f"{title}\n\n{body}")
|
||||
@@ -1 +1 @@
|
||||
4.4.24
|
||||
4.4.16
|
||||
|
||||
Reference in New Issue
Block a user