takane
Validators for ArtifactDB file formats
Loading...
Searching...
No Matches
genomic_ranges.hpp
Go to the documentation of this file.
1#ifndef TAKANE_GENOMIC_RANGES_HPP
2#define TAKANE_GENOMIC_RANGES_HPP
3
4#include "H5Cpp.h"
5#include "ritsuko/ritsuko.hpp"
6#include "sanisizer/sanisizer.hpp"
7
8#include <string>
9#include <filesystem>
10#include <stdexcept>
11#include <exception>
12#include <cstdint>
13#include <cstddef>
14#include <type_traits>
15#include <limits>
16#include <optional>
17
18#include "utils_string.hpp"
19#include "utils_public.hpp"
20#include "utils_other.hpp"
21#include "utils_json.hpp"
22#include "utils_missing.hpp"
23
29namespace takane {
30
34void validate(const std::filesystem::path&, const ObjectMetadata&, const Options& options);
35bool derived_from(const std::string&, const std::string&, const Options& options);
36
37struct SequenceLimits {
38 SequenceLimits(std::size_t n) :
39 circular(sanisizer::cast<I<decltype(circular.size())> >(n)),
40 length(sanisizer::cast<I<decltype(length.size())> >(n))
41 {}
42
43 std::vector<std::optional<bool> > circular;
44 std::vector<std::optional<std::uint64_t> > length;
45};
46
47inline SequenceLimits find_sequence_limits(const std::filesystem::path& path, const Options& options) {
48 const std::string type_name = "sequence_information";
49
50 auto smeta = read_object_metadata(path);
51 if (!derived_from(smeta.type, type_name, options)) {
52 throw std::runtime_error("expected a 'sequence_information' object or one of its subclasses");
53 }
54 validate(path, smeta, options);
55
56 // No need for checks here, we assume everything is now valid.
57 H5::H5File handle(path / "info.h5", H5F_ACC_RDONLY);
58 auto ghandle = handle.openGroup(type_name);
59
60 auto lhandle = ghandle.openDataSet("length");
61 auto lspace = lhandle.getSpace();
62 hsize_t num_seq;
63 lspace.getSimpleExtentDims(&num_seq);
64
65 SequenceLimits output(num_seq);
66 ritsuko::hdf5::Stream1dNumericDatasetOptions opt;
67 opt.contiguous_chunk_size = options.hdf5_buffer_size;
68
69 ritsuko::hdf5::Stream1dNumericDataset<std::uint64_t> lstream(&lhandle, num_seq, opt);
70 auto lmissing = read_numeric_missing_placeholder<std::uint64_t>(lhandle, "missing-value-placeholder");
71 iterate_stream<std::uint64_t>(
72 lstream,
73 [&](hsize_t i, std::uint64_t l) -> void {
74 if (!lmissing.has_value() || l != *lmissing) {
75 output.length[i] = l;
76 }
77 }
78 );
79
80 auto chandle = ghandle.openDataSet("circular");
81 ritsuko::hdf5::Stream1dNumericDataset<std::int32_t> cstream(&chandle, num_seq, opt);
82 auto cmissing = read_numeric_missing_placeholder<std::int32_t>(chandle, "missing-value-placeholder");
83 iterate_stream<std::int32_t>(
84 cstream,
85 [&](hsize_t i, std::int32_t c) -> void {
86 if (!cmissing.has_value() || c != *cmissing) {
87 output.circular[i] = c;
88 }
89 }
90 );
91
92 return output;
93}
94
95inline bool start_plus_width_exceeds_int64(std::int64_t start, std::uint64_t width) {
96 constexpr std::uint64_t end_limit = std::numeric_limits<std::int64_t>::max();
97 if (start >= 0) {
98 // That is, does 'start + width > end_limit'?
99 // No underflow as 'start <= end_limit', so 'end_limit - start >= 0'.
100 return sanisizer::is_less_than(end_limit - start, width);
101 }
102
103 const std::uint64_t abs_start = static_cast<std::uint64_t>(-(1 + start)) + 1; // effectively '-start' but avoid overflow of the signed type.
104 if (abs_start >= width) {
105 // 'start + width == width - abs_start', which would be non-positive in this case.
106 // So it can't possibly exceed end_limit.
107 return false;
108 }
109
110 return sanisizer::is_less_than(end_limit, width - abs_start);
111}
121inline void validate_genomic_ranges(const std::filesystem::path& path, const ObjectMetadata& metadata, const Options& options) {
122 const std::string type_name = "genomic_ranges"; // use a separate variable to avoid dangling reference warnings from GCC.
123
124 try {
125 const auto& type_meta = extract_json_object(metadata.other, type_name);
126 try {
127 const auto& vstring = extract_json_version_string(type_meta);
128 auto version = ritsuko::parse_version_string(vstring.c_str(), vstring.size(), /* skip_patch = */ true);
129 if (version.major != 1) {
130 throw std::runtime_error("unsupported version string '" + vstring + "'");
131 }
132 } catch (...) {
133 std::throw_with_nested(std::runtime_error("failed to validate 'version'"));
134 }
135 } catch (...) {
136 std::throw_with_nested(std::runtime_error("failed to read '" + type_name + "' in the object metadata"));
137 }
138
139 // Figuring out the sequence length constraints.
140 std::optional<SequenceLimits> limits;
141 try {
142 limits = find_sequence_limits(path / "sequence_information", options);
143 } catch (...) {
144 std::throw_with_nested(std::runtime_error("failed to read the 'sequence_information' object"));
145 }
146
147 const auto num_sequences = limits->length.size();
148
149 // Now loading all three components.
150 hsize_t num_ranges;
151 try {
152 H5::H5File handle(path / "ranges.h5", H5F_ACC_RDONLY);
153 auto ghandle = handle.openGroup(type_name);
154
155 H5::DataSet id_handle;
156 try {
157 id_handle = ghandle.openDataSet("sequence");
158 if (ritsuko::hdf5::exceeds_integer_limit(id_handle, 64, false)) {
159 throw std::runtime_error("expected a datatype that fits into a 64-bit unsigned integer");
160 }
161
162 auto id_space = id_handle.getSpace();
163 if (id_space.getSimpleExtentNdims() != 1) {
164 throw std::runtime_error("expected a 1-dimensional dataset");
165 }
166 id_space.getSimpleExtentDims(&num_ranges);
167 } catch (...) {
168 std::throw_with_nested(std::runtime_error("failed to validate 'sequence'"));
169 }
170
171 H5::DataSet start_handle;
172 try {
173 start_handle = ghandle.openDataSet("start");
174 if (ritsuko::hdf5::exceeds_integer_limit(start_handle, 64, true)) {
175 throw std::runtime_error("expected a datatype that fits into a 64-bit signed integer");
176 }
177
178 auto start_space = start_handle.getSpace();
179 if (start_space.getSimpleExtentNdims() != 1) {
180 throw std::runtime_error("expected a 1-dimensional dataset");
181 }
182 hsize_t num_start;
183 start_space.getSimpleExtentDims(&num_start);
184 if (num_start != num_ranges) {
185 throw std::runtime_error("dataset extent should be the same as that of 'sequence'");
186 }
187 } catch (...) {
188 std::throw_with_nested(std::runtime_error("failed to validate 'start'"));
189 }
190
191 H5::DataSet width_handle;
192 try {
193 width_handle = ghandle.openDataSet("width");
194 if (ritsuko::hdf5::exceeds_integer_limit(width_handle, 64, false)) {
195 throw std::runtime_error("expected 'width' to have a datatype that fits into a 64-bit unsigned integer");
196 }
197
198 auto width_space = width_handle.getSpace();
199 if (width_space.getSimpleExtentNdims() != 1) {
200 throw std::runtime_error("expected a 1-dimensional dataset");
201 }
202 hsize_t num_width;
203 width_space.getSimpleExtentDims(&num_width);
204 if (num_width != num_ranges) {
205 throw std::runtime_error("dataset extent should be the same as that of 'sequence'");
206 }
207 } catch (...) {
208 std::throw_with_nested(std::runtime_error("failed to validate 'width'"));
209 }
210
211 ritsuko::hdf5::Stream1dNumericDatasetOptions opt;
212 opt.contiguous_chunk_size = options.hdf5_buffer_size;
213 StreamIterator<std::uint64_t> id_stream(ritsuko::hdf5::Stream1dNumericDataset<std::uint64_t>(&id_handle, num_ranges, opt));
214 StreamIterator<std::int64_t> start_stream(ritsuko::hdf5::Stream1dNumericDataset<std::int64_t>(&start_handle, num_ranges, opt));
215 StreamIterator<std::uint64_t> width_stream(ritsuko::hdf5::Stream1dNumericDataset<std::uint64_t>(&width_handle, num_ranges, opt));
216
217 for (I<decltype(num_ranges)> i = 0; i < num_ranges; ++i) {
218 const auto id = id_stream.next();
219 if (sanisizer::is_greater_than_or_equal(id, num_sequences)) {
220 throw std::runtime_error("'sequence' must be less than the number of sequences in 'sequence_information'");
221 }
222
223 const auto start = start_stream.next();
224 const auto width = width_stream.next();
225
226 // If it's definitely non-circular, the start position should be positive.
227 const auto& circular = limits->circular[id];
228 if (circular.has_value() && !(*circular)) {
229 if (start < 1) {
230 throw std::runtime_error("non-positive 'start' position for a non-circular sequence");
231 }
232
233 const auto& length = limits->length[id];
234 if (length.has_value()) {
235 if (sanisizer::is_greater_than(start, *length)) {
236 throw std::runtime_error("'start' position exceeds sequence length for a non-circular sequence");
237 }
238
239 // End position is computed as 'start + width - 1', which should be <= length.
240 // The LHS should not overflow as 'start >= 1' and 'start <= length' so '0 <= length - start + 1 <= length'.
241 if (sanisizer::is_less_than(*length - (start - 1), width)) {
242 throw std::runtime_error("end position exceeds sequence length for a non-circular sequence");
243 }
244 }
245 }
246
247 if (start_plus_width_exceeds_int64(start, width)) {
248 throw std::runtime_error("sum of 'start' and 'width' is beyond the range of a 64-bit signed integer");
249 }
250 }
251
252 try {
253 auto strand_handle = ghandle.openDataSet("strand");
254 if (ritsuko::hdf5::exceeds_integer_limit(strand_handle, 32, true)) {
255 throw std::runtime_error("expected a datatype that fits into a 32-bit signed integer");
256 }
257
258 auto strand_space = strand_handle.getSpace();
259 if (strand_space.getSimpleExtentNdims() != 1) {
260 throw std::runtime_error("expected a 1-dimensional dataset");
261 }
262 hsize_t nstrand;
263 strand_space.getSimpleExtentDims(&nstrand);
264 if (nstrand != num_ranges) {
265 throw std::runtime_error("dataset extent should be the same as that of 'sequence'");
266 }
267
268 ritsuko::hdf5::Stream1dNumericDataset<std::int32_t> strand_stream(&strand_handle, num_ranges, opt);
269 iterate_stream<std::int32_t>(
270 strand_stream,
271 [&](hsize_t, std::int32_t x) -> void {
272 if (x < -1 || x > 1) {
273 throw std::runtime_error("entries should be one of 0, -1, or 1");
274 }
275 }
276 );
277 } catch (...) {
278 std::throw_with_nested(std::runtime_error("failed to validate 'strand'"));
279 }
280
281 validate_names(ghandle, "name", num_ranges, options.hdf5_buffer_size);
282 } catch (...) {
283 std::throw_with_nested(std::runtime_error("failed to validate '" + type_name + "' in 'ranges.h5'"));
284 }
285
286 validate_mcols(path, "range_annotations", num_ranges, options);
287 validate_metadata(path, "other_annotations", options);
288}
289
296inline std::size_t height_of_genomic_ranges(const std::filesystem::path& path, [[maybe_unused]] const ObjectMetadata& metadata, [[maybe_unused]] const Options& options) {
297 H5::H5File handle(path / "ranges.h5", H5F_ACC_RDONLY);
298 auto ghandle = handle.openGroup("genomic_ranges");
299 auto dhandle = ghandle.openDataSet("sequence");
300 hsize_t output;
301 dhandle.getSpace().getSimpleExtentDims(&output);
302 return sanisizer::cast<std::size_t>(output);
303}
304
305}
306
307#endif
takane validation functions.
Definition _derived_from.hpp:15
ObjectMetadata read_object_metadata(const std::filesystem::path &path)
Definition utils_public.hpp:77
std::size_t height_of_genomic_ranges(const std::filesystem::path &path, const ObjectMetadata &metadata, const Options &options)
Definition genomic_ranges.hpp:296
void validate_genomic_ranges(const std::filesystem::path &path, const ObjectMetadata &metadata, const Options &options)
Definition genomic_ranges.hpp:121
bool derived_from(const std::string &type, const std::string &base, const Options &options)
Definition _derived_from.hpp:80
void validate(const std::filesystem::path &path, const ObjectMetadata &metadata, const Options &options)
Definition _validate.hpp:107
Object metadata, including the type and other fields.
Definition utils_public.hpp:29
std::unordered_map< std::string, std::shared_ptr< millijson::Base > > other
Definition utils_public.hpp:38
Validation options.
Definition utils_public.hpp:99
hsize_t hdf5_buffer_size
Definition utils_public.hpp:109
Exported utilities.