Skip to content

Commit f5f211b

Browse files
committed
opt to print rgfa stats table
1 parent 16e3d61 commit f5f211b

3 files changed

Lines changed: 194 additions & 84 deletions

File tree

src/rgfa.cpp

Lines changed: 167 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#include <sstream>
33
#include <algorithm>
44
#include <queue>
5+
#include <iomanip>
6+
#include <set>
57

68
//#define debug
79

@@ -126,7 +128,8 @@ void RGFACover::compute(const PathHandleGraph* graph,
126128
nid_t node_id = graph->get_id(graph->get_handle_of_step(step_handle));
127129
if (node_to_interval.count(node_id)) {
128130
cerr << "[rgfa error]: node " << node_id << " covered by two reference paths,"
129-
<< " including " << graph->get_path_name(ref_path_handle)
131+
<< " including " << graph->get_path_name(ref_path_handle) << " and "
132+
<< graph->get_path_name(graph->get_path_handle_of_step(rgfa_intervals.at(node_to_interval.at(node_id)).first))
130133
<< ". rGFA support current requires disjoint acyclic reference paths" << endl;
131134
exit(1);
132135
}
@@ -930,6 +933,24 @@ vector<pair<int64_t, nid_t>> RGFACover::get_reference_nodes(nid_t node_id, bool
930933
return output_reference_nodes;
931934
}
932935

936+
pair<handle_t, handle_t> RGFACover::parse_variant_id(const string& variant_id) const {
937+
938+
if (variant_id[0] == '>' || variant_id[0] == '<') {
939+
size_t p = variant_id.find_first_of("<>", 2);
940+
if (p != string::npos) {
941+
int64_t id1 = parse<int64_t>(variant_id.substr(1, p-1));
942+
int64_t id2 = parse<int64_t>(variant_id.substr(p+1));
943+
if (!graph->has_node(id1) || !graph->has_node(id2)) {
944+
throw runtime_error("Unable to parse " + variant_id + ": node(s) not found in graph");
945+
}
946+
return make_pair(graph->get_handle(id1, variant_id[0] == '<'),
947+
graph->get_handle(id2, variant_id[p] == '<'));
948+
}
949+
}
950+
throw runtime_error("Failed to parse " + variant_id);
951+
return make_pair(handle_t(), handle_t());
952+
}
953+
933954
void RGFACover::annotate_vcf(vcflib::VariantCallFile& vcf, ostream& os) {
934955
vcflib::Variant var(vcf);
935956

@@ -951,12 +972,6 @@ void RGFACover::annotate_vcf(vcflib::VariantCallFile& vcf, ostream& os) {
951972
}
952973
}
953974

954-
// also want an rGFA path lookup
955-
unordered_map<string, path_handle_t> name_to_rgfa_path;
956-
graph->for_each_path_of_sample(RGFACover::rgfa_sample_name, [&](path_handle_t path_handle) {
957-
name_to_rgfa_path[RGFACover::revert_rgfa_path_name(graph->get_path_name(path_handle))] = path_handle;
958-
});
959-
960975
// remove header lines we're going to add
961976
vcf.removeInfoHeaderLine("R_CHROM");
962977
vcf.removeInfoHeaderLine("R_START");
@@ -971,80 +986,156 @@ void RGFACover::annotate_vcf(vcflib::VariantCallFile& vcf, ostream& os) {
971986

972987
os << vcf.header << endl;
973988

974-
string prev_sequence_name;
975-
string prev_r_chrom;
976-
string prev_r_start;
977-
string prev_r_end;
978-
string prev_rank;
979989
while (vcf.getNextVariant(var)) {
980-
if (name_to_rgfa_path.count(var.sequenceName)) {
981-
string r_chrom;
982-
string r_start;
983-
string r_end;
984-
string rank;
985-
if (var.sequenceName == prev_sequence_name) {
986-
// just use the previous values, which will be the same
987-
r_chrom = prev_r_chrom;
988-
r_start = prev_r_start;
989-
r_end = prev_r_end;
990-
rank = prev_rank;
991-
} else {
992-
// compute from the cover
993-
path_handle_t path_handle = name_to_rgfa_path.at(var.sequenceName);
994-
nid_t first_node = graph->get_id(graph->get_handle_of_step(graph->path_begin(path_handle)));
995-
vector<pair<int64_t, nid_t>> ref_nodes = this->get_reference_nodes(first_node, false);
996-
997-
int64_t min_ref_pos = numeric_limits<int64_t>::max();
998-
int64_t max_ref_pos = -1;
999-
int64_t min_rank = numeric_limits<int64_t>::max();
1000-
for (const pair<int64_t, nid_t>& rank_node : ref_nodes) {
1001-
step_handle_t ref_step = this->rgfa_intervals.at(node_to_interval.at(rank_node.second)).first;
1002-
path_handle_t ref_path = graph->get_path_handle_of_step(ref_step);
1003-
string name = graph->get_path_name(ref_path);
1004-
// we assume one reference contig (which is built into the whole structure)
1005-
assert(r_chrom.empty() || r_chrom == name);
1006-
r_chrom = name;
1007-
int64_t ref_pos = node_to_ref_pos.at(rank_node.second);
1008-
// assume snarl is forward on both reference nodes
1009-
// todo: this won't be exact for some inversion cases, I don't think --
1010-
// need to test these and either add check / or move to oriented search
1011-
min_ref_pos = min(min_ref_pos, ref_pos + (int64_t)graph->get_length(graph->get_handle(rank_node.second)));
1012-
max_ref_pos = max(max_ref_pos, ref_pos);
1013-
min_rank = min(min_rank, rank_node.first);
1014-
}
990+
pair<handle_t, handle_t> id_handles = parse_variant_id(var.id);
991+
string r_chrom;
992+
string r_start;
993+
string r_end;
994+
string rank;
995+
996+
// compute from the cover
997+
nid_t first_node = graph->get_id(id_handles.first);
998+
vector<pair<int64_t, nid_t>> ref_nodes = this->get_reference_nodes(first_node, false);
999+
1000+
int64_t min_ref_pos = numeric_limits<int64_t>::max();
1001+
int64_t max_ref_pos = -1;
1002+
int64_t min_rank = numeric_limits<int64_t>::max();
1003+
for (const pair<int64_t, nid_t>& rank_node : ref_nodes) {
1004+
step_handle_t ref_step = this->rgfa_intervals.at(node_to_interval.at(rank_node.second)).first;
1005+
path_handle_t ref_path = graph->get_path_handle_of_step(ref_step);
1006+
string name = graph->get_path_name(ref_path);
1007+
// we assume one reference contig (which is built into the whole structure)
1008+
assert(r_chrom.empty() || r_chrom == name);
1009+
r_chrom = name;
1010+
int64_t ref_pos = node_to_ref_pos.at(rank_node.second);
1011+
// assume snarl is forward on both reference nodes
1012+
// todo: this won't be exact for some inversion cases, I don't think --
1013+
// need to test these and either add check / or move to oriented search
1014+
min_ref_pos = min(min_ref_pos, ref_pos + (int64_t)graph->get_length(graph->get_handle(rank_node.second)));
1015+
max_ref_pos = max(max_ref_pos, ref_pos);
1016+
min_rank = min(min_rank, rank_node.first);
1017+
}
10151018

1016-
r_start = std::to_string(min_ref_pos);
1017-
r_end = std::to_string(max_ref_pos);
1018-
rank = std::to_string(min_rank);
1019-
}
1019+
r_start = std::to_string(min_ref_pos);
1020+
r_end = std::to_string(max_ref_pos);
1021+
rank = std::to_string(min_rank);
1022+
1023+
var.info["R_CHROM"] = {r_chrom};
1024+
var.info["R_START"] = {r_start};
1025+
var.info["R_END"] = {r_end};
1026+
var.info["RANK"] = {rank};
1027+
1028+
os << var << endl;
1029+
}
1030+
}
10201031

1021-
if (!var.info.count("R_CHROM")) {
1022-
var.format.push_back("R_CHROM");
1023-
}
1024-
var.info["R_CHROM"] = {r_chrom};
1025-
if (!var.info.count("R_START")) {
1026-
var.format.push_back("R_START");
1027-
}
1028-
var.info["R_START"] = {r_start};
1029-
if (!var.info.count("R_END")) {
1030-
var.format.push_back("R_END");
1031-
}
1032-
var.info["R_END"] = {r_end};
1033-
if (!var.info.count("RANK")) {
1034-
var.format.push_back("RANK");
1032+
void RGFACover::print_stats(ostream& os) {
1033+
1034+
// todo: mostly copied from annotate function above. should probably refactor common
1035+
// logic into its own, shared thing.y
1036+
unordered_map<nid_t, int64_t> node_to_ref_pos;
1037+
for (int64_t i = 0; i < this->num_ref_intervals; ++i) {
1038+
const pair<step_handle_t, step_handle_t>& ref_interval = this->rgfa_intervals.at(i);
1039+
// assumption: ref intervals span entire path
1040+
assert(graph->path_begin(graph->get_path_handle_of_step(ref_interval.first)) == ref_interval.first);
1041+
int64_t pos = 0;
1042+
for (step_handle_t step_handle = ref_interval.first; step_handle != ref_interval.second;
1043+
step_handle = graph->get_next_step(step_handle)) {
1044+
handle_t handle = graph->get_handle_of_step(step_handle);
1045+
nid_t node_id = graph->get_id(handle);
1046+
assert(!graph->get_is_reverse(handle));
1047+
assert(!node_to_ref_pos.count(node_id));
1048+
node_to_ref_pos[node_id] = pos;
1049+
pos += graph->get_length(handle);
1050+
}
1051+
}
1052+
1053+
// the header
1054+
os << "#Sample" << "\t"
1055+
<< "Haplotype" << "\t"
1056+
<< "Locus" << "\t"
1057+
<< "Start" << "\t"
1058+
<< "End" << "\t"
1059+
<< "NodeStart" << "\t"
1060+
<< "NodeEnd" << "\t"
1061+
<< "Rank" << "\t"
1062+
<< "AvgDepth" << "\t"
1063+
<< "AvgSampleDepth" << "\t"
1064+
<< "RefSequence" << "\t"
1065+
<< "RefStart" << "\t"
1066+
<< "RefEnd" << endl;
1067+
1068+
for (int64_t i = this->num_ref_intervals; i < this->rgfa_intervals.size(); ++i) {
1069+
const pair<step_handle_t, step_handle_t>& interval = this->rgfa_intervals[i];
1070+
path_handle_t path_handle = graph->get_path_handle_of_step(interval.first);
1071+
subrange_t rgfa_subrange = graph->get_subrange(path_handle);
1072+
assert(rgfa_subrange != PathMetadata::NO_SUBRANGE);
1073+
1074+
int64_t path_length = 0;
1075+
int64_t tot_depth = 0;
1076+
int64_t tot_sample_depth = 0;
1077+
int64_t tot_steps = 0;
1078+
for (step_handle_t step = interval.first; step != interval.second; step = graph->get_next_step(step)) {
1079+
path_length += graph->get_length(graph->get_handle_of_step(step));
1080+
set<string> sample_set;
1081+
vector<step_handle_t> steps = graph->steps_of_handle(graph->get_handle_of_step(step));
1082+
for (step_handle_t& other_step : steps) {
1083+
path_handle_t other_path = graph->get_path_handle_of_step(other_step);
1084+
sample_set.insert(graph->get_sample_name(other_path));
10351085
}
1036-
var.info["RANK"] = {rank};
1037-
1038-
prev_sequence_name = var.sequenceName;
1039-
prev_r_chrom = r_chrom;
1040-
prev_r_start = r_start;
1041-
prev_r_end = r_end;
1042-
prev_rank = rank;
1043-
1044-
} else {
1045-
//cerr << "rGFA [warning]: VCF reference " << var.sequenceName << " not found in graph" << endl;
1086+
tot_depth += steps.size();
1087+
tot_sample_depth += sample_set.size();
1088+
// we don't want to count the rgfa cover
1089+
// todo (can remove this when move away from path-based scheme)
1090+
assert(sample_set.count(RGFACover::rgfa_sample_name));
1091+
--tot_depth;
1092+
--tot_sample_depth;
1093+
++tot_steps;
10461094
}
1047-
os << var << endl;
1095+
pair<string, string> sample_locus = RGFACover::parse_rgfa_locus_name(graph->get_locus_name(path_handle));
1096+
size_t haplotype = graph->get_haplotype(path_handle);
1097+
1098+
nid_t first_node = graph->get_id(graph->get_handle_of_step(interval.first));
1099+
vector<pair<int64_t, nid_t>> ref_nodes = this->get_reference_nodes(first_node, false);
1100+
// interval is open ended, so we go back to last node
1101+
handle_t last_handle = graph->get_handle_of_step(graph->get_previous_step(interval.second));
1102+
1103+
int64_t min_ref_pos = numeric_limits<int64_t>::max();
1104+
int64_t max_ref_pos = -1;
1105+
int64_t min_rank = numeric_limits<int64_t>::max();
1106+
string r_chrom;
1107+
for (const pair<int64_t, nid_t>& rank_node : ref_nodes) {
1108+
step_handle_t ref_step = this->rgfa_intervals.at(node_to_interval.at(rank_node.second)).first;
1109+
path_handle_t ref_path = graph->get_path_handle_of_step(ref_step);
1110+
string name = graph->get_path_name(ref_path);
1111+
// we assume one reference contig (which is built into the whole structure)
1112+
assert(r_chrom.empty() || r_chrom == name);
1113+
r_chrom = name;
1114+
int64_t ref_pos = node_to_ref_pos.at(rank_node.second);
1115+
// assume snarl is forward on both reference nodes
1116+
// todo: this won't be exact for some inversion cases, I don't think --
1117+
// need to test these and either add check / or move to oriented search
1118+
min_ref_pos = min(min_ref_pos, ref_pos + (int64_t)graph->get_length(graph->get_handle(rank_node.second)));
1119+
max_ref_pos = max(max_ref_pos, ref_pos);
1120+
min_rank = min(min_rank, rank_node.first);
1121+
}
1122+
1123+
1124+
os << sample_locus.first << "\t"
1125+
<< sample_locus.second << "\t"
1126+
<< (haplotype != PathMetadata::NO_HAPLOTYPE ? (int64_t)haplotype : (int64_t)-1) << "\t"
1127+
<< rgfa_subrange.first << "\t"
1128+
<< (rgfa_subrange.first + path_length) << "\t"
1129+
<< (graph->get_is_reverse(graph->get_handle_of_step(interval.first)) ? "<" : ">")
1130+
<< graph->get_id(graph->get_handle_of_step(interval.first)) << "\t"
1131+
<< (graph->get_is_reverse(last_handle) ? "<" : ">")
1132+
<< graph->get_id(last_handle) << "\t"
1133+
<< min_rank << "\t"
1134+
<< std::fixed << std::setprecision(2) << (tot_depth / tot_steps) << "\t"
1135+
<< std::fixed << std::setprecision(2) << (tot_sample_depth / tot_steps) << "\t"
1136+
<< Paths::strip_subrange(r_chrom) << "\t"
1137+
<< min_ref_pos << "\t"
1138+
<< max_ref_pos << "\n";
10481139
}
10491140
}
10501141

src/rgfa.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,14 @@ class RGFACover {
8585
// return nullptr if node not in an interval
8686
const pair<step_handle_t, step_handle_t>* get_interval(nid_t node_id) const;
8787

88+
// parse an id of the form >244>2334 and return the pair of handles
89+
pair<handle_t, handle_t> parse_variant_id(const string& variant_id) const;
90+
8891
// add R_CHROM, R_START, R_END, F_LEN tags to a VCF using the cover
8992
void annotate_vcf(vcflib::VariantCallFile& vcf, ostream& os);
93+
94+
// print out a table of statistics
95+
void print_stats(ostream& os);
9096

9197
protected:
9298

src/subcommand/paths_main.cpp

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ void help_paths(char** argv) {
4545
<< " -s, --snarls FILE snarls (from vg snarls) to avoid recomputing. snarls only used for rgfa cover." << endl
4646
<< " -t, --threads N use up to N threads when computing rGFA cover (default: all available)" << endl
4747
<< " --rgfa-vcf FILE add rGFA cover tags to VCF (which must be called on rGFA cover in graph from -f)" << endl
48+
<< " --rgfa-intervals print out table of rgfa intervals and their ranks and positions" << endl
4849
<< " output path data:" << endl
4950
<< " -X, --extract-gam print (as GAM alignments) the stored paths in the graph" << endl
5051
<< " -A, --extract-gaf print (as GAF alignments) the stored paths in the graph" << endl
@@ -99,6 +100,7 @@ unordered_map<PathSense, string> SENSE_TO_STRING {
99100
// Long options with no corresponding short options.
100101
const int OPT_SELECT_RGFA_FRAGMENTS = 1000;
101102
const int OPT_ANNOTATE_RGFA_VCF = 1001;
103+
const int OPT_RGFA_INTERVALS = 1002;
102104

103105
int main_paths(int argc, char** argv) {
104106

@@ -116,6 +118,7 @@ int main_paths(int argc, char** argv) {
116118
bool retain_paths = false;
117119
int64_t rgfa_min_len = -1;
118120
string rgfa_vcf_filename;
121+
bool rgfa_print_intervals = false;
119122
string snarl_filename;
120123
string graph_file;
121124
string gbwt_file;
@@ -165,6 +168,7 @@ int main_paths(int argc, char** argv) {
165168
{"coverage", no_argument, 0, 'c'},
166169
{"rgfa-paths", no_argument, 0, OPT_SELECT_RGFA_FRAGMENTS},
167170
{"rgfa-vcf", required_argument, 0, OPT_ANNOTATE_RGFA_VCF},
171+
{"rgfa-intervals", no_argument, 0, OPT_RGFA_INTERVALS},
168172
{"threads", required_argument, 0, 't'},
169173
// Hidden options for backward compatibility.
170174
{"threads-by", required_argument, 0, 'q'},
@@ -302,6 +306,11 @@ int main_paths(int argc, char** argv) {
302306
rgfa_vcf_filename = optarg;
303307
output_formats++;
304308
break;
309+
310+
case OPT_RGFA_INTERVALS:
311+
rgfa_print_intervals = true;
312+
output_formats++;
313+
break;
305314

306315
case 't':
307316
{
@@ -365,7 +374,7 @@ int main_paths(int argc, char** argv) {
365374
}
366375
}
367376
if (output_formats != 1) {
368-
std::cerr << "error: [vg paths] one output format (-X, -A, -V, -d, -r, -R, -L, -F, -E, -C, -c, --rgfa-vcf) must be specified" << std::endl;
377+
std::cerr << "error: [vg paths] one output format (-X, -A, -V, -d, -r, -R, -L, -F, -E, -C, -c, --rgfa-vcf, --rgfa-intervals) must be specified" << std::endl;
369378
std::exit(EXIT_FAILURE);
370379
}
371380
if (selection_criteria > 1) {
@@ -673,7 +682,7 @@ int main_paths(int argc, char** argv) {
673682

674683
// output the graph
675684
vg::io::save_handle_graph(graph.get(), std::cout, reference_path_names);
676-
} else if (!rgfa_vcf_filename.empty()) {
685+
} else if (!rgfa_vcf_filename.empty() || rgfa_print_intervals) {
677686
RGFACover rgfa_cover;
678687
// load up the rank-0 reference path selection
679688
unordered_set<path_handle_t> reference_paths;
@@ -686,13 +695,17 @@ int main_paths(int argc, char** argv) {
686695
cerr << "error[vg paths]: no rGFA cover found in graph. Please compute one with -f before annotating a VCF" << endl;
687696
exit(1);
688697
}
689-
vcflib::VariantCallFile variant_file;
690-
variant_file.open(rgfa_vcf_filename);
691-
if (!variant_file.is_open()) {
692-
cerr << "error[vg paths]: unable to open VCF file: " << rgfa_vcf_filename << endl;
693-
exit(1);
698+
if (!rgfa_vcf_filename.empty()) {
699+
vcflib::VariantCallFile variant_file;
700+
variant_file.open(rgfa_vcf_filename);
701+
if (!variant_file.is_open()) {
702+
cerr << "error[vg paths]: unable to open VCF file: " << rgfa_vcf_filename << endl;
703+
exit(1);
704+
}
705+
rgfa_cover.annotate_vcf(variant_file, cout);
706+
} else if (rgfa_print_intervals) {
707+
rgfa_cover.print_stats(cout);
694708
}
695-
rgfa_cover.annotate_vcf(variant_file, cout);
696709
} else if (coverage) {
697710
// for every node, count the number of unique paths. then add the coverage count to each one
698711
// (we're doing the whole graph here, which could be inefficient in the case the user is selecting

0 commit comments

Comments
 (0)