1#ifndef TAKANE_SEQUENCE_STRING_SET_HPP
2#define TAKANE_SEQUENCE_STRING_SET_HPP
4#include "byteme/byteme.hpp"
5#include "ritsuko/ritsuko.hpp"
6#include "sanisizer/sanisizer.hpp"
16#include "utils_other.hpp"
28inline constexpr int char2int(
char val) {
29 return static_cast<int>(val) -
static_cast<int>(std::numeric_limits<char>::min());
32typedef std::array<bool, char2int(std::numeric_limits<char>::max())> AllowedCharacterMap;
34template<
bool has_quality_>
35std::size_t parse_sequence_contents(
const std::filesystem::path& path, AllowedCharacterMap allowed,
char lowest_quality,
bool parallel) {
36 auto gzreader = open_reader<byteme::GzipFileReader>(path, byteme::GzipFileReaderOptions());
37 auto pb = wrap_reader_for_bytes<char>(std::move(gzreader), parallel);
40 unsigned long long line_count = 1;
41 auto advance_and_check = [&]() ->
char {
43 throw std::runtime_error(
"premature end of the file at line " + ritsuko::itoa(line_count));
50 while (pb->get() ==
'\n') {
62 if constexpr(!has_quality_) {
63 if (pb->get() !=
'>') {
64 throw std::runtime_error(
"sequence name should start with '>' at line " + ritsuko::itoa(line_count));
67 if (pb->get() !=
'@') {
68 throw std::runtime_error(
"sequence name should start with '@' at line " + ritsuko::itoa(line_count));
72 const std::size_t mult_limit = nseq / 10;
73 const std::size_t add_limit = nseq % 10;
74 std::size_t proposed = 0;
77 char val = advance_and_check();
83 if (val <
'0' || val >
'9') {
84 throw std::runtime_error(
"sequence name should be a non-negative integer at line " + ritsuko::itoa(line_count));
87 const auto delta = val -
'0';
88 if (proposed < mult_limit || (proposed == mult_limit && sanisizer::is_less_than_or_equal(delta, add_limit))) {
92 throw std::runtime_error(
"sequence name is out of bounds at line " + ritsuko::itoa(line_count));
95 if (empty || proposed != nseq) {
96 throw std::runtime_error(
"sequence name should be its index at line " + ritsuko::itoa(line_count));
101 if constexpr(!has_quality_) {
103 char val = advance_and_check();
107 if (!pb->advance()) {
114 }
else if (allowed[char2int(val)]) {
115 val = advance_and_check();
117 throw std::runtime_error(
"forbidden character '" + std::string(1, val) +
"' in sequence at line " + ritsuko::itoa(line_count));
123 std::size_t seq_length = 0;
125 char val = advance_and_check();
129 val = advance_and_check();
133 }
else if (allowed[char2int(val)]) {
134 seq_length = sanisizer::sum<std::size_t>(seq_length, 1);
135 val = advance_and_check();
137 throw std::runtime_error(
"forbidden character '" + std::string(1, val) +
"' in sequence at line " + ritsuko::itoa(line_count));
144 const char val = advance_and_check();
154 std::size_t qual_length = 0;
156 const char val = advance_and_check();
159 if (qual_length >= seq_length) {
162 }
else if (val >= lowest_quality && val <=
'~') {
163 qual_length = sanisizer::sum<std::size_t>(qual_length, 1);
165 throw std::runtime_error(
"out-of-range quality score '" + std::string(1, val) +
"' detected at line " + ritsuko::itoa(line_count));
169 if (qual_length != seq_length) {
170 throw std::runtime_error(
"unequal lengths for quality and sequence strings at line " + ritsuko::itoa(line_count));
174 nseq = sanisizer::sum<std::size_t>(nseq, 1);
180inline std::size_t parse_sequence_names(
const std::filesystem::path& path,
bool parallel) {
181 auto gzreader = open_reader<byteme::GzipFileReader>(path, byteme::GzipFileReaderOptions());
182 auto pb = wrap_reader_for_bytes<char>(std::move(gzreader), parallel);
184 std::size_t nseq = 0;
185 unsigned long long line_count = 1;
186 auto advance_and_check = [&]() ->
char {
187 if (!pb->advance()) {
188 throw std::runtime_error(
"premature end of the file at line " + ritsuko::itoa(line_count));
193 while (pb->valid()) {
194 char val = pb->get();
196 throw std::runtime_error(
"name should start with a quote at line " + ritsuko::itoa(line_count));
200 val = advance_and_check();
202 val = advance_and_check();
208 }
else if (val !=
'"') {
209 throw std::runtime_error(
"characters present after end quote at line " + ritsuko::itoa(line_count));
211 }
else if (val ==
'\n') {
229 const std::string type_name =
"sequence_string_set";
231 std::size_t expected_nseq = 0;
232 AllowedCharacterMap allowed{};
233 bool has_qualities =
false;
234 char lowest_quality = 0;
237 const auto& obj = extract_json_object(metadata.
other, type_name);
239 const auto& vstring = extract_json_version_string(obj);
240 auto version = ritsuko::parse_version_string(vstring.c_str(), vstring.size(),
true);
241 if (version.major != 1) {
242 throw std::runtime_error(
"unsupported version string '" + vstring +
"'");
245 std::throw_with_nested(std::runtime_error(
"failed to read 'version'"));
249 auto lIt = obj.find(
"length");
250 if (lIt == obj.end()) {
251 throw std::runtime_error(
"no such property");
254 const auto& val = lIt->second;
255 if (val->type() != millijson::NUMBER) {
256 throw std::runtime_error(
"property should be a JSON number");
259 auto num =
reinterpret_cast<const millijson::Number*
>(val.get())->value();
260 if (num < 0 || std::floor(num) != num) {
261 throw std::runtime_error(
"property should be a non-negative integer");
264 expected_nseq = sanisizer::from_float<std::size_t>(num);
266 std::throw_with_nested(std::runtime_error(
"failed to read 'length'"));
270 const std::string stype_name =
"sequence_type";
271 const auto& stype = extract_json_string(obj, stype_name);
273 std::string allowable;
274 if (stype ==
"DNA" || stype ==
"RNA") {
275 allowable =
"ACGRYSWKMBDHVN";
276 if (stype ==
"DNA") {
281 }
else if (stype ==
"AA") {
282 allowable =
"ACDEFGHIKLMNPQRSTVWY";
283 }
else if (stype ==
"custom") {
284 std::fill(allowed.begin() + char2int(
'!'), allowed.begin() + char2int(
'~') + 1,
true);
286 throw std::runtime_error(
"unknown sequence type '" + stype +
"'");
289 const auto lower_shift =
'A' -
'a';
290 for (
auto a : allowable) {
291 allowed[char2int(a)] =
true;
292 allowed[char2int(a) - lower_shift] =
true;
294 allowed[char2int(
'.')] =
true;
295 allowed[char2int(
'-')] =
true;
298 std::throw_with_nested(std::runtime_error(
"failed to extract 'sequence_type'"));
301 bool offset_failed =
false;
303 auto xIt = obj.find(
"quality_type");
304 if (xIt != obj.end()) {
305 const auto& val = xIt->second;
306 if (val->type() != millijson::STRING) {
307 throw std::runtime_error(
"expected a JSON string");
310 const auto& qtype =
reinterpret_cast<const millijson::String*
>(val.get())->value();
312 if (qtype ==
"phred") {
313 has_qualities =
true;
314 offset_failed =
true;
316 auto oIt = obj.find(
"quality_offset");
317 if (oIt == obj.end()) {
318 throw std::runtime_error(
"no such property");
321 const auto& val = oIt->second;
322 if (val->type() != millijson::NUMBER) {
323 throw std::runtime_error(
"property should be a JSON number");
326 double offset =
reinterpret_cast<const millijson::Number*
>(val.get())->value();
328 lowest_quality =
'!';
329 }
else if (offset == 64) {
330 lowest_quality =
'@';
332 throw std::runtime_error(
"property should be either 33 or 64");
335 std::throw_with_nested(std::runtime_error(
"failed to extract 'quality_offset'"));
338 }
else if (qtype ==
"solexa") {
339 has_qualities =
true;
340 lowest_quality =
';';
342 }
else if (qtype !=
"none") {
343 throw std::runtime_error(
"unknown quality type '" + qtype +
"'");
350 std::throw_with_nested(std::runtime_error(
"failed to extract 'quality_type'"));
354 std::throw_with_nested(std::runtime_error(
"failed to read '" + type_name +
"' in the object metadata"));
359 std::size_t nseq = 0;
361 fname =
"sequences.fastq.gz";
362 nseq = parse_sequence_contents<true>(path / fname, allowed, lowest_quality, options.
parallel_reads);
364 fname =
"sequences.fasta.gz";
365 nseq = parse_sequence_contents<false>(path / fname, allowed, lowest_quality, options.
parallel_reads);
367 if (nseq != expected_nseq) {
368 throw std::runtime_error(
"number of sequences is different from the expected length (" + ritsuko::itoa(nseq) +
" versus " + ritsuko::itoa(expected_nseq) +
")");
371 std::throw_with_nested(std::runtime_error(
"failed to validate '" + fname +
"'"));
374 auto npath = path /
"names.txt.gz";
375 if (std::filesystem::exists(npath)) {
377 std::size_t nnames = parse_sequence_names(npath, options.
parallel_reads);
378 if (nnames != expected_nseq) {
379 throw std::runtime_error(
"number of names is different from the number of sequences (" + ritsuko::itoa(nnames) +
" versus " + ritsuko::itoa(expected_nseq) +
")");
382 std::throw_with_nested(std::runtime_error(
"failed to validate '" + npath.filename().string() +
"'"));
386 validate_mcols(path,
"sequence_annotations", expected_nseq, options);
387 validate_metadata(path,
"other_annotations", options);
397 const std::string type_name =
"sequence_string_set";
398 const auto& obj = extract_json_object(metadata.
other, type_name);
399 auto lIt = obj.find(
"length");
400 const auto& val = lIt->second;
401 return sanisizer::from_float<std::size_t>(
reinterpret_cast<const millijson::Number*
>(val.get())->value());
takane validation functions.
Definition _derived_from.hpp:15
void validate_sequence_string_set(const std::filesystem::path &path, const ObjectMetadata &metadata, const Options &options)
Definition sequence_string_set.hpp:228
std::size_t height_of_sequence_string_set(const std::filesystem::path &path, const ObjectMetadata &metadata, const Options &options)
Definition sequence_string_set.hpp:396
Validation options.
Definition utils_public.hpp:99
bool parallel_reads
Definition utils_public.hpp:103