diff --git a/main.cpp b/main.cpp index 88517ede7fcd3678bb98d0a2bda8052cb82c6700..ee24b1273b1f334b452defcaa3d6472fefb40cb4 100644 --- a/main.cpp +++ b/main.cpp @@ -1,6 +1,8 @@ +#include <array> #include <iostream> #include <fstream> #include <numeric> +#include <vector> #include "gtest/gtest.h" @@ -252,6 +254,18 @@ TEST(cell_tree, json_load) { } // SWC tests +void expect_cell_equals(const neuron::io::cell_record &expected, + const neuron::io::cell_record &actual) +{ + EXPECT_EQ(expected.id(), actual.id()); + EXPECT_EQ(expected.type(), actual.type()); + EXPECT_FLOAT_EQ(expected.x(), actual.x()); + EXPECT_FLOAT_EQ(expected.y(), actual.y()); + EXPECT_FLOAT_EQ(expected.z(), actual.z()); + EXPECT_FLOAT_EQ(expected.radius(), actual.radius()); + EXPECT_EQ(expected.parent(), actual.parent()); +} + TEST(cell_record, construction) { using namespace neuron::io; @@ -308,21 +322,14 @@ TEST(cell_record, construction) EXPECT_EQ(cell.z(), 1.); EXPECT_EQ(cell.radius(), 1.); EXPECT_EQ(cell.diameter(), 2*1.); - EXPECT_EQ(cell.parent_id(), -1); + EXPECT_EQ(cell.parent(), -1); } { // check copy constructor - cell_record proto_cell(cell_record::custom, 0, 1., 1., 1., 1., -1); - cell_record cell(proto_cell); - EXPECT_EQ(cell.id(), 0); - EXPECT_EQ(cell.type(), cell_record::custom); - EXPECT_EQ(cell.x(), 1.); - EXPECT_EQ(cell.y(), 1.); - EXPECT_EQ(cell.z(), 1.); - EXPECT_EQ(cell.radius(), 1.); - EXPECT_EQ(cell.diameter(), 2*1.); - EXPECT_EQ(cell.parent_id(), -1); + cell_record cell_orig(cell_record::custom, 0, 1., 1., 1., 1., -1); + cell_record cell(cell_orig); + expect_cell_equals(cell_orig, cell); } } @@ -330,20 +337,6 @@ TEST(swc_parser, invalid_input) { using namespace neuron::io; - { - // check empty file - cell_record cell; - std::istringstream is(""); - EXPECT_THROW(is >> cell, std::runtime_error); - } - - { - // check comment-only file - cell_record cell; - std::istringstream is("#comment\n#comment\n"); - EXPECT_THROW(is >> cell, std::runtime_error); - } - { // check incomplete lines; missing parent std::istringstream is("1 1 14.566132 34.873772 7.857000 0.717830\n"); @@ -351,14 +344,6 @@ TEST(swc_parser, invalid_input) EXPECT_THROW(is >> cell, std::logic_error); } - { - // check incomplete lines; missing newline - // FIXME: we should probably accept such files - std::istringstream is("1 1 14.566132 34.873772 7.857000 0.717830 -1"); - cell_record cell; - EXPECT_THROW(is >> cell, std::runtime_error); - } - { // Check long lines std::istringstream is(std::string(256, 'a') + "\n"); @@ -387,31 +372,65 @@ TEST(swc_parser, valid_input) using namespace neuron::io; { - // check valid input - std::istringstream is("\ -# this is a comment\n\ -# this is a comment\n\ -1 1 14.566132 34.873772 7.857000 0.717830 -1 # end-of-line comment\n\ -"); - cell_record cell; + // check empty file; no cell may be parsed + cell_record cell, cell_orig; + std::istringstream is(""); EXPECT_NO_THROW(is >> cell); - EXPECT_EQ(cell.id(), 0); // zero-based indexing - EXPECT_EQ(cell.type(), cell_record::soma); - EXPECT_FLOAT_EQ(cell.x(), 14.566132); - EXPECT_FLOAT_EQ(cell.y(), 34.873772); - EXPECT_FLOAT_EQ(cell.z(), 7.857000); - EXPECT_FLOAT_EQ(cell.radius(), 0.717830); - EXPECT_FLOAT_EQ(cell.parent_id(), -1); + expect_cell_equals(cell_orig, cell); } { - // Test multiple records + // check comment-only file not ending with a newline; + // no cell may be parsed + cell_record cell, cell_orig; + std::istringstream is("#comment\n#comment"); + EXPECT_NO_THROW(is >> cell); + expect_cell_equals(cell_orig, cell); } + { - // Test input ending with comments + // check last line case (no newline at the end) + std::istringstream is("1 1 14.566132 34.873772 7.857000 0.717830 -1"); + cell_record cell; + EXPECT_NO_THROW(is >> cell); + EXPECT_EQ(0, cell.id()); // zero-based indexing + EXPECT_EQ(cell_record::soma, cell.type()); + EXPECT_FLOAT_EQ(14.566132, cell.x()); + EXPECT_FLOAT_EQ(34.873772, cell.y()); + EXPECT_FLOAT_EQ( 7.857000, cell.z()); + EXPECT_FLOAT_EQ( 0.717830, cell.radius()); + EXPECT_FLOAT_EQ( -1, cell.parent()); } + { + // check valid input with a series of records + std::vector<cell_record> cells_orig = { + cell_record(cell_record::soma, 0, + 14.566132, 34.873772, 7.857000, 0.717830, -1), + cell_record(cell_record::dendrite, 1, + 14.566132+1, 34.873772+1, 7.857000+1, 0.717830+1, -1) + }; + + std::stringstream swc_input; + swc_input << "# this is a comment\n"; + swc_input << "# this is a comment\n"; + for (auto c : cells_orig) + swc_input << c << "\n"; + + swc_input << "# this is a final comment\n"; + try { + std::size_t nr_records = 0; + cell_record cell; + while ( !(swc_input >> cell).eof()) { + ASSERT_LT(nr_records, cells_orig.size()); + expect_cell_equals(cells_orig[nr_records], cell); + ++nr_records; + } + } catch (std::exception &e) { + ADD_FAILURE(); + } + } } int main(int argc, char **argv) { diff --git a/swcio.hpp b/swcio.hpp index 7e9725b7e872b37338cad4f0d107688d9af8d0f9..9b2756f811be9366b9c8cb924f3c47924312ba92 100644 --- a/swcio.hpp +++ b/swcio.hpp @@ -1,5 +1,6 @@ #pragma once +#include <cmath> #include <exception> #include <iostream> #include <sstream> @@ -68,48 +69,62 @@ public: , y_(0) , z_(0) , r_(0) - , parent_id_(0) + , parent_id_(-1) { } cell_record(const cell_record &other) = default; cell_record &operator=(const cell_record &other) = default; - kind type() + friend bool operator==(const cell_record &lhs, + const cell_record &rhs) + { + return lhs.id_ == rhs.id_; + } + + friend bool operator!=(const cell_record &lhs, + const cell_record &rhs) + { + return !(lhs == rhs); + } + + friend std::ostream &operator<<(std::ostream &os, const cell_record &cell); + + kind type() const { return type_; } - int id() + int id() const { return id_; } - int parent_id() + int parent() const { return parent_id_; } - float x() + float x() const { return x_; } - float y() + float y() const { return y_; } - float z() + float z() const { return z_; } - float radius() + float radius() const { return r_; } - float diameter() + float diameter() const { return 2*r_; } @@ -136,7 +151,6 @@ public: { init_linebuff(); } - swc_parser() : delim_(" ") @@ -152,7 +166,7 @@ public: delete[] linebuff_; } - cell_record parse_record(std::istream &is) + std::istream &parse_record(std::istream &is, cell_record &cell) { while (!is.eof() && !is.bad()) { // consume empty and comment lines first @@ -161,17 +175,21 @@ public: break; } - if (is.eof()) - throw std::runtime_error("unexpected eof found"); - if (is.bad()) - throw std::runtime_error("i/o error"); + // let the caller check for such events + return is; + + if (is.eof() && + (linebuff_[0] == 0 || linebuff_[0] == comment_prefix_)) + // last line is either empty or a comment; don't parse anything + return is; if (is.fail() && is.gcount() == max_line_ - 1) throw std::runtime_error("too long line detected"); std::istringstream line(linebuff_); - return parse_record(line); + cell = parse_record(line); + return is; } private: @@ -186,16 +204,15 @@ private: // If we try to read past the eof; fail bit will also be set // FIXME: better throw a custom parse_error exception throw std::logic_error("could not parse value"); - - if (is.bad()) - throw std::runtime_error("i/o error"); } + // FIXME: need not to be a member function template<typename T> T parse_value_strict(std::istream &is) { T val; is >> val; + // std::cout << val << "\n"; check_parse_status(is); // everything's fine @@ -246,12 +263,23 @@ cell_record swc_parser::parse_record(std::istringstream &is) std::istream &operator>>(std::istream &is, cell_record &cell) { swc_parser parser; - cell = parser.parse_record(is); + parser.parse_record(is, cell); return is; } -std::ostream &operator<<(std::ostream &os, const cell_record &cell); - +std::ostream &operator<<(std::ostream &os, const cell_record &cell) +{ + // output in one-based indexing + os << cell.id_+1 << " " + << cell.type_ << " " + << std::setprecision(7) << cell.x_ << " " + << std::setprecision(7) << cell.y_ << " " + << std::setprecision(7) << cell.z_ << " " + << std::setprecision(7) << cell.r_ << " " + << ((cell.parent_id_ == -1) ? cell.parent_id_ : cell.parent_id_+1); + + return os; +} } // end of neuron::io } // end of neuron