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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
if: ${{ github.repository == 'asl/BandageNG' && github.ref == 'refs/heads/dev' }}
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
# Fetch all history for all branches and tags
fetch-depth: 0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_TOKEN }}
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ env:
BUILD_TYPE: Release
QT_QPA_PLATFORM: offscreen
XDG_RUNTIME_DIR: '/tmp/runtime-runner'
QT_VERSION: 6.7.3
QT_VERSION: 6.9.2

jobs:
build-linux:
Expand All @@ -23,15 +23,15 @@ jobs:
uses: pr-mpt/actions-commit-hash@v3

- name: '🧰 Checkout'
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
submodules: recursive

- name: '⚙️ Grab cmake & ninja'
uses: lukka/get-cmake@latest

- name: '⚙️ Install Qt'
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
cache: true
cache-key-prefix: ${{ runner.os }}-QtCache
Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
uses: lukka/get-cmake@latest

- name: '⚙️ Install Qt'
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
cache: true
cache-key-prefix: ${{ runner.os }}-QtCache
Expand Down Expand Up @@ -133,7 +133,7 @@ jobs:
arch: x64

- name: '⚙️ Install Qt'
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
cache: true
cache-key-prefix: ${{ runner.os }}-QtCache
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ env:
BUILD_TYPE: Release
QT_QPA_PLATFORM: offscreen
XDG_RUNTIME_DIR: '/tmp/runtime-runner'
QT_VERSION: 6.7.3
QT_VERSION: 6.9.2

jobs:
build:
Expand All @@ -37,7 +37,7 @@ jobs:
shell: bash

- name: Checkout repository and submodules
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
submodules: recursive

Expand All @@ -54,7 +54,7 @@ jobs:
arch: x64

- name: Install Qt
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
cache: true
cache-key-prefix: ${{ runner.os }}-QtCache
Expand Down
3 changes: 2 additions & 1 deletion command_line/image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ int handleImageCmd(QApplication *app,
g_settings->startingNodes,
g_settings->minDepthRange, g_settings->maxDepthRange,
&g_blastSearch->queries(), "all",
"", g_settings->nodeDistance);
g_settings->startingPaths, g_settings->nodeDistance,
g_settings->startingWalks);
std::vector<DeBruijnNode *> startingNodes = graph::getStartingNodes(&errorTitle, &errorMessage,
*g_assemblyGraph, scope);
if (!errorMessage.isEmpty()) {
Expand Down
3 changes: 2 additions & 1 deletion command_line/layout.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ int handleLayoutCmd(QApplication *app,
g_settings->startingNodes,
g_settings->minDepthRange, g_settings->maxDepthRange,
&g_blastSearch->queries(), "all",
"", g_settings->nodeDistance);
g_settings->startingPaths, g_settings->nodeDistance,
g_settings->startingWalks);
std::vector<DeBruijnNode *> startingNodes = graph::getStartingNodes(&errorTitle, &errorMessage,
*g_assemblyGraph, scope);
if (!errorMessage.isEmpty()) {
Expand Down
3 changes: 2 additions & 1 deletion command_line/reduce.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ int handleReduceCmd(QApplication *app,
g_settings->startingNodes,
g_settings->minDepthRange, g_settings->maxDepthRange,
&g_blastSearch->queries(), "all",
"", g_settings->nodeDistance);
g_settings->startingPaths, g_settings->nodeDistance,
g_settings->startingWalks);
auto startingNodes = graph::getStartingNodes(&errorTitle, &errorMessage,
*g_assemblyGraph, scope);
if (!errorMessage.isEmpty()) {
Expand Down
21 changes: 17 additions & 4 deletions command_line/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,24 +205,30 @@ static CLI::App *addGraphScopeSettings(CLI::App &app) {
auto *scope = app.add_option_group("Graph scope",
"These settings control the graph scope. "
"If the aroundnodes scope is used, then the --nodes option must also be used. "
"If the aroundblast scope is used, a BLAST query must be given with the --query option.");
"If the aroundblast scope is used, a BLAST query must be given with the --query option. "
"If the aroundcomponent scope is used, then at least one of --nodes, --path or --walk must be used.");

scope->add_option("--scope", g_settings->graphScope, "Graph scope, from one of the following options: entire, aroundnodes, aroundblast, depthrange")
scope->add_option("--scope", g_settings->graphScope, "Graph scope, from one of the following options: entire, aroundnodes, aroundblast, depthrange, aroundcomponent")
->transform(CLI::CheckedTransformer(
std::vector<std::pair<std::string, GraphScope>>{
{"entire", GraphScope::WHOLE_GRAPH},
{"aroundnodes", GraphScope::AROUND_NODE},
// FIXME: paths!
{"aroundblast", GraphScope::AROUND_BLAST_HITS},
{"depthrange", GraphScope::DEPTH_RANGE}}))
{"depthrange", GraphScope::DEPTH_RANGE},
{"aroundcomponent", GraphScope::AROUND_COMPONENT}}))
->default_val("entire");
scope->add_flag("--exact,!--partial", g_settings->startingNodesExactMatch, "Choose between exact or partial node name matching (default: exact)");
add_setting(*scope, "--distance", g_settings->nodeDistance, "The number of node steps away to draw for the aroundnodes and aroundblast scopes");
add_setting(*scope, "--mindepth", g_settings->minDepthRange, "The minimum allowed depth for the depthrange scope");
add_setting(*scope, "--maxdepth", g_settings->maxDepthRange, "The maximum allowed depth for the depthrange scope");
scope->add_option("--query", g_settings->blastQueryFilename, "A FASTA file of either nucleotide or protein sequences to be used as BLAST queries")
->check(CLI::ExistingFile);
scope->add_option("--nodes", g_settings->startingNodes, "A comma-separated list of starting nodes for the aroundnodes scope (default: none)")
scope->add_option("--nodes", g_settings->startingNodes, "A comma-separated list of starting nodes for the aroundnodes and aroundcomponent scopes (default: none)")
->capture_default_str();
scope->add_option("--path", g_settings->startingPaths, "A comma-separated list of path names used as seeds for the aroundcomponent scope (default: none)")
->capture_default_str();
scope->add_option("--walk", g_settings->startingWalks, "A comma-separated list of walk names used as seeds for the aroundcomponent scope (default: none)")
->capture_default_str();

scope->callback([scope]() {
Expand All @@ -234,6 +240,13 @@ static CLI::App *addGraphScopeSettings(CLI::App &app) {
throw CLI::ValidationError("Bandage-NG error",
"A list of starting nodes must be given with the --nodes option\nwhen the aroundnodes scope is used.");
break;
case AROUND_COMPONENT:
if (g_settings->startingNodes.isEmpty() &&
g_settings->startingPaths.isEmpty() &&
g_settings->startingWalks.isEmpty())
throw CLI::ValidationError("Bandage-NG error",
"At least one of --nodes, --path or --walk must be given\nwhen the aroundcomponent scope is used.");
break;
case AROUND_BLAST_HITS:
if (g_settings->blastQueryFilename.isEmpty())
throw CLI::ValidationError("Bandage-NG error",
Expand Down
42 changes: 42 additions & 0 deletions graph/assemblygraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
#include <cmath>
#include <utility>
#include <deque>
#include <unordered_set>

AssemblyGraph::AssemblyGraph()
: m_sequencesLoadedFromFasta(NOT_READY)
Expand Down Expand Up @@ -541,6 +542,47 @@ void AssemblyGraph::markNodesToDraw(const graph::Scope &scope,
if (entry->isPositiveNode() || g_settings->doubleMode)
entry->setAsDrawn();
}
} else if (scope.graphScope() == AROUND_COMPONENT) {
auto nodeToDraw = [](DeBruijnNode *node) {
if (!g_settings->doubleMode && node->isNegativeNode())
return node->getReverseComplement();
return node;
};

std::unordered_set<DeBruijnNode *> seen;
std::vector<DeBruijnNode *> queue;
queue.reserve(startingNodes.size() * 2);

for (auto *node : startingNodes) {
DeBruijnNode *drawn = nodeToDraw(node);
drawn->setAsDrawn();
drawn->setAsSpecial();

auto enqueue = [&](DeBruijnNode *n) {
if (n && seen.insert(n).second)
queue.push_back(n);
};
enqueue(node);
enqueue(node->getReverseComplement());
}

while (!queue.empty()) {
DeBruijnNode *node = queue.back();
queue.pop_back();
nodeToDraw(node)->setAsDrawn();

for (auto *edge : node->edges()) {
DeBruijnNode *other = edge->getOtherNode(node);
if (!other)
continue;
if (seen.insert(other).second)
queue.push_back(other);
if (DeBruijnNode *otherRc = other->getReverseComplement()) {
if (seen.insert(otherRc).second)
queue.push_back(otherRc);
}
}
}
} else {
for (auto *node : startingNodes) {
//If we are in single mode, make sure that each node is positive.
Expand Down
106 changes: 104 additions & 2 deletions graph/graphscope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,33 @@
#include "program/settings.h"

#include "assemblygraph.h"
#include "path.h"

#include <unordered_set>
#include <vector>

namespace graph {
static QStringList splitCommaSeparated(QString text) {
text = text.simplified();
QStringList tokens = text.split(',');
QStringList out;
for (QString token : tokens) {
token = token.trimmed();
if (!token.isEmpty())
out.push_back(token);
}
return out;
}

static void appendUnique(std::vector<DeBruijnNode *> &nodes,
std::unordered_set<DeBruijnNode *> &seen,
const std::vector<DeBruijnNode *> &more) {
for (auto *node : more) {
if (seen.insert(node).second)
nodes.push_back(node);
}
}

std::vector<DeBruijnNode *>
getStartingNodes(QString *errorTitle, QString *errorMessage,
const AssemblyGraph &graph, const Scope &graphScope) {
Expand Down Expand Up @@ -74,6 +98,74 @@ namespace graph {

return pathIt->walk.nodes();
}
case AROUND_COMPONENT: {
QStringList pathNames = splitCommaSeparated(graphScope.componentPaths());
QStringList walkNames = splitCommaSeparated(graphScope.componentWalks());
bool hasNodes = !AssemblyGraph::checkIfStringHasNodes(graphScope.componentNodes());

if (!hasNodes && pathNames.empty() && walkNames.empty()) {
*errorTitle = "No starting nodes";
*errorMessage = "Please enter at least one node, path or walk when drawing the graph using the "
"'Around connected component' scope. Separate multiple names with commas.";
return {};
}

std::unordered_set<DeBruijnNode *> seen;
std::vector<DeBruijnNode *> nodesInGraph;

if (hasNodes) {
std::vector<QString> nodesNotInGraph;
auto fromNodes = graph.getNodesFromStringList(graphScope.componentNodes(),
g_settings->startingNodesExactMatch,
&nodesNotInGraph);
if (!nodesNotInGraph.empty()) {
*errorTitle = "Nodes not found";
*errorMessage =
AssemblyGraph::generateNodesNotFoundErrorMessage(nodesNotInGraph,
g_settings->startingNodesExactMatch);
return {};
}
appendUnique(nodesInGraph, seen, fromNodes);
}

QStringList missingPaths;
for (const QString &name : pathNames) {
auto pathIt = graph.m_deBruijnGraphPaths.find(name.toStdString());
if (pathIt == graph.m_deBruijnGraphPaths.end()) {
missingPaths.push_back(name);
continue;
}
appendUnique(nodesInGraph, seen, pathIt->nodes());
}
if (!missingPaths.empty()) {
*errorTitle = "Invalid path";
*errorMessage = "No path with such name is loaded: " + missingPaths.join(", ");
return {};
}

QStringList missingWalks;
for (const QString &name : walkNames) {
auto walkIt = graph.m_deBruijnGraphWalks.find(name.toStdString());
if (walkIt == graph.m_deBruijnGraphWalks.end()) {
missingWalks.push_back(name);
continue;
}
appendUnique(nodesInGraph, seen, walkIt->walk.nodes());
}
if (!missingWalks.empty()) {
*errorTitle = "Invalid walk";
*errorMessage = "No walk with such sequence name is loaded: " + missingWalks.join(", ");
return {};
}

if (nodesInGraph.empty()) {
*errorTitle = "No starting nodes";
*errorMessage = "The specified nodes, paths and walks did not match anything in the graph.";
return {};
}

return nodesInGraph;
}
case AROUND_BLAST_HITS: {
std::vector<DeBruijnNode *> startingNodes;
if (const auto *queries = graphScope.queries())
Expand Down Expand Up @@ -106,7 +198,8 @@ namespace graph {
Scope scope(GraphScope graphScope, const QString &nodesList,
double minDepthRange, double maxDepthRange,
const search::Queries *blastQueries, const QString &blastQueryName,
const QString &pathName, unsigned distance) {
const QString &pathName, unsigned distance,
const QString &walkName) {
switch (graphScope) {
case WHOLE_GRAPH:
return Scope::wholeGraph();
Expand All @@ -115,11 +208,13 @@ namespace graph {
case AROUND_PATHS:
return Scope::aroundPath(pathName, distance);
case AROUND_WALKS:
return Scope::aroundWalk(pathName, distance);
return Scope::aroundWalk(walkName.isEmpty() ? pathName : walkName, distance);
case AROUND_BLAST_HITS:
return Scope::aroundHits(blastQueries, blastQueryName, distance);
case DEPTH_RANGE:
return Scope::depthRange(minDepthRange, maxDepthRange);
case AROUND_COMPONENT:
return Scope::aroundComponent(nodesList, pathName, walkName);
}

assert(0 && "Invalid scope!");
Expand All @@ -140,4 +235,11 @@ namespace graph {

return res;
}

Scope Scope::aroundComponent(QString nodes, QString paths, QString walks) {
Scope res;
res.m_scope = AROUND_COMPONENT;
res.m_opt = ComponentSeed{std::move(nodes), std::move(paths), std::move(walks)};
return res;
}
}
Loading
Loading