diff --git a/include/morphio/enums.h b/include/morphio/enums.h index 048a8be30..19424cfa8 100644 --- a/include/morphio/enums.h +++ b/include/morphio/enums.h @@ -15,11 +15,15 @@ enum LogLevel { ERROR, WARNING, INFO, DEBUG }; /** The list of modifier flags that can be passed when loading a morphology * See morphio::mut::modifiers for more information **/ enum Option { - NO_MODIFIER = 0x00, //!< Read morphology as is without any modification - TWO_POINTS_SECTIONS = 0x01, //!< Read sections only with 2 or more points - SOMA_SPHERE = 0x02, //!< Interpret morphology soma as a sphere - NO_DUPLICATES = 0x04, //!< Skip duplicating points - NRN_ORDER = 0x08 //!< Order of neurites will be the same as in NEURON simulator + NO_MODIFIER = 0, //!< Read morphology as is without any modification + TWO_POINTS_SECTIONS = 1 << 0, //!< Keep only the first and last points of sections + SOMA_SPHERE = 1 << 1, //!< Interpret morphology soma as a sphere + NO_DUPLICATES = 1 << 2, //!< Skip duplicating points + NRN_ORDER = 1 << 3, //!< Order of neurites will be the same as in NEURON simulator + ALLOW_ROOT_BIFURCATIONS = 1 << 4, //!< Bifurcations at first point are allowed + ALLOW_SOMA_BIFURCATIONS = 1 << 5, //!< Bifurcations in soma are allowed + ALLOW_MULTIPLE_SOMATA = 1 << 6, //!< Multiple somata are allowed + ALLOW_CUSTOM_ROOT_ID = 1 << 7 //!< Custom root points are allowed }; /** @@ -41,6 +45,10 @@ enum Warning { ZERO_DIAMETER, //!< Zero section diameter SOMA_NON_CONTOUR, //!< Soma must be a contour for ASC and H5 SOMA_NON_CYLINDER_OR_POINT, //!< Soma must be stacked cylinders or a point + ROOT_BIFURCATION, //!< Bifurcation at root point + SOMA_BIFURCATION, //!< Bifurcation in soma + MULTIPLE_SOMATA, //!< Multiple somata + CUSTOM_ROOT_ID, //!< Custom root ID }; enum AnnotationType { diff --git a/include/morphio/errorMessages.h b/include/morphio/errorMessages.h index aaa15dbe9..3b4cdf708 100644 --- a/include/morphio/errorMessages.h +++ b/include/morphio/errorMessages.h @@ -232,6 +232,14 @@ class ErrorMessages std::string WARNING_MITOCHONDRIA_WRITE_NOT_SUPPORTED() const; /** Writing without soma warning message */ std::string WARNING_WRITE_NO_SOMA() const; + /** Loading soma with a bifurcation */ + std::string WARNING_SOMA_BIFURCATION() const; + /** Loading multiple somata */ + std::string WARNING_MULTIPLE_SOMATA() const; + /** Loading point with root ID no equal to -1 */ + std::string WARNING_CUSTOM_ROOT_ID(const Sample& sample) const; + /** Loading section with only one point */ + std::string WARNING_ROOT_BIFURCATION(const Sample& sample) const; /** Writing empty morphology warning message */ std::string WARNING_WRITE_EMPTY_MORPHOLOGY() const; /** Soma not found warning message */ @@ -244,7 +252,8 @@ class ErrorMessages std::string WARNING_WRONG_DUPLICATE(const std::shared_ptr& current, const std::shared_ptr& parent) const; /** Writing empty section warning message */ - std::string WARNING_APPENDING_EMPTY_SECTION(std::shared_ptr); + std::string WARNING_APPENDING_EMPTY_SECTION( + const std::shared_ptr& section) const; /** Writing single child section warning message */ std::string WARNING_ONLY_CHILD(const DebugInfo& info, unsigned int parentId, @@ -253,7 +262,7 @@ class ErrorMessages /** Soma does not conform NeuroMorpho warning message */ std::string WARNING_NEUROMORPHO_SOMA_NON_CONFORM(const Sample& root, const Sample& child1, - const Sample& child2); + const Sample& child2) const; /** Wrong root point warning message */ std::string WARNING_WRONG_ROOT_POINT(const std::vector& children) const; diff --git a/src/errorMessages.cpp b/src/errorMessages.cpp index 569e6bce8..70bbdd2ac 100644 --- a/src/errorMessages.cpp +++ b/src/errorMessages.cpp @@ -284,6 +284,14 @@ std::string ErrorMessages::WARNING_WRITE_NO_SOMA() const { return errorMsg(0, ErrorLevel::WARNING, "Warning: writing file without a soma"); } +std::string ErrorMessages::WARNING_SOMA_BIFURCATION() const { + return errorMsg(0, ErrorLevel::WARNING, "Warning: found soma bifurcation"); +} + +std::string ErrorMessages::WARNING_MULTIPLE_SOMATA() const { + return errorMsg(0, ErrorLevel::WARNING, "Warning: found multiple somata"); +} + std::string ErrorMessages::WARNING_WRITE_EMPTY_MORPHOLOGY() const { return errorMsg(0, ErrorLevel::WARNING, @@ -307,12 +315,26 @@ std::string ErrorMessages::WARNING_DISCONNECTED_NEURITE(const Sample& sample) co } std::string ErrorMessages::WARNING_APPENDING_EMPTY_SECTION( - std::shared_ptr section) { + const std::shared_ptr& section) const { return errorMsg(0, ErrorLevel::WARNING, "Warning: appending empty section with id: " + std::to_string(section->id())); } +std::string ErrorMessages::WARNING_CUSTOM_ROOT_ID(const Sample& sample) const { + return errorMsg(sample.lineNumber, + ErrorLevel::WARNING, + "Warning: root id changed from " + std::to_string(sample.parentId) + + " to -1 for point with id: " + std::to_string(sample.id)); +} + +std::string ErrorMessages::WARNING_ROOT_BIFURCATION(const Sample& sample) const { + return errorMsg(sample.lineNumber, + ErrorLevel::WARNING, + "Warning: root bifurcation kept for point with id: " + + std::to_string(sample.id)); +} + std::string ErrorMessages::WARNING_WRONG_DUPLICATE( const std::shared_ptr& current, const std::shared_ptr& parent) const { @@ -374,7 +396,7 @@ std::string ErrorMessages::WARNING_ONLY_CHILD(const DebugInfo& info, std::string ErrorMessages::WARNING_NEUROMORPHO_SOMA_NON_CONFORM(const Sample& root, const Sample& child1, - const Sample& child2) { + const Sample& child2) const { floatType x = root.point[0], y = root.point[1], z = root.point[2], r = root.diameter / 2; std::stringstream ss; ss << "Warning: the soma does not conform the three point soma spec\n" diff --git a/src/readers/morphologyASC.cpp b/src/readers/morphologyASC.cpp index fdebb9c4a..33807d25c 100644 --- a/src/readers/morphologyASC.cpp +++ b/src/readers/morphologyASC.cpp @@ -72,9 +72,9 @@ class NeurolucidaParser NeurolucidaParser(NeurolucidaParser const&) = delete; NeurolucidaParser& operator=(NeurolucidaParser const&) = delete; - morphio::mut::Morphology& parse(const std::string& input) { + morphio::mut::Morphology& parse(const std::string& input, unsigned int& options) { lex_.start_parse(input); - parse_root_sexps(); + parse_root_sexps(options); return nb_; } @@ -112,12 +112,12 @@ class NeurolucidaParser return {{point[0], point[1], point[2]}, point[3]}; } - bool parse_neurite_branch(Header& header) { + bool parse_neurite_branch(Header& header, unsigned int& options) { lex_.consume(Token::LPAREN, "New branch should start with LPAREN"); bool ret = true; while (true) { - ret &= parse_neurite_section(header); + ret &= parse_neurite_section(header, options); if (lex_.ended() || (lex_.current()->id != +Token::PIPE && lex_.current()->id != +Token::LPAREN)) { break; @@ -130,7 +130,8 @@ class NeurolucidaParser int32_t _create_soma_or_section(const Header& header, std::vector& points, - std::vector& diameters) { + std::vector& diameters, + unsigned int& options) { int32_t return_id = -1; morphio::Property::PointLevel properties; properties._points = points; @@ -144,9 +145,24 @@ class NeurolucidaParser nb_.addMarker(marker); return_id = -1; } else if (header.token == Token::CELLBODY) { - if (!nb_.soma()->points().empty()) - throw SomaError(err_.ERROR_SOMA_ALREADY_DEFINED(lex_.line_num())); - nb_.soma()->properties() = properties; + if (!nb_.soma()->points().empty()) { + if (options & ALLOW_SOMA_BIFURCATIONS) { + printError(Warning::SOMA_BIFURCATION, err_.WARNING_SOMA_BIFURCATION()); + } else if (options & ALLOW_MULTIPLE_SOMATA) { + printError(Warning::MULTIPLE_SOMATA, err_.WARNING_MULTIPLE_SOMATA()); + } else { + throw SomaError(err_.ERROR_SOMA_ALREADY_DEFINED(lex_.line_num())); + } + nb_.soma()->properties()._points.insert(nb_.soma()->properties()._points.end(), + properties._points.begin(), + properties._points.end()); + nb_.soma()->properties()._diameters.insert( + nb_.soma()->properties()._diameters.end(), + properties._diameters.begin(), + properties._diameters.end()); + } else { + nb_.soma()->properties() = properties; + } return_id = -1; } else { SectionType section_type = TokenToSectionType(header.token); @@ -273,7 +289,7 @@ class NeurolucidaParser } - bool parse_neurite_section(const Header& header) { + bool parse_neurite_section(const Header& header, unsigned int& options) { Points points; std::vector diameters; auto section_id = static_cast(nb_.sections().size()); @@ -286,7 +302,7 @@ class NeurolucidaParser throw RawDataError(err_.ERROR_EOF_IN_NEURITE(lex_.line_num())); } else if (is_end_of_section(id)) { if (!points.empty()) { - _create_soma_or_section(header, points, diameters); + _create_soma_or_section(header, points, diameters, options); } return true; } else if (is_end_of_branch(id)) { @@ -320,7 +336,7 @@ class NeurolucidaParser marker_header.token = Token::STRING; marker_header.label = lex_.peek()->str(); lex_.consume_until(Token::LPAREN); - parse_neurite_section(marker_header); + parse_neurite_section(marker_header, options); lex_.consume(Token::RPAREN, "Marker should end with RPAREN"); } else if (peek_id == +Token::NUMBER) { Point point; @@ -330,11 +346,11 @@ class NeurolucidaParser diameters.push_back(radius); } else if (peek_id == +Token::LPAREN) { if (!points.empty()) { - section_id = _create_soma_or_section(header, points, diameters); + section_id = _create_soma_or_section(header, points, diameters, options); } Header child_header = header; child_header.parent_id = section_id; - parse_neurite_branch(child_header); + parse_neurite_branch(child_header, options); } else { throw RawDataError( err_.ERROR_UNKNOWN_TOKEN(lex_.line_num(), lex_.peek()->str())); @@ -347,14 +363,14 @@ class NeurolucidaParser } } - void parse_root_sexps() { + void parse_root_sexps(unsigned int& options) { // parse the top level blocks, and if they are a neurite, otherwise skip while (!lex_.ended()) { if (static_cast(lex_.current()->id) == Token::LPAREN) { lex_.consume(); const Header header = parse_root_sexp_header(); if (lex_.current()->id != +Token::RPAREN) { - parse_neurite_section(header); + parse_neurite_section(header, options); } } @@ -378,7 +394,7 @@ Property::Properties load(const std::string& path, unsigned int options) { NeurolucidaParser parser(path); - morphio::mut::Morphology& nb_ = parser.parse(contents); + morphio::mut::Morphology& nb_ = parser.parse(contents, options); nb_.applyModifiers(options); Property::Properties properties = nb_.buildReadOnly(); diff --git a/src/readers/morphologySWC.cpp b/src/readers/morphologySWC.cpp index bdc971f1a..0dc90afdd 100644 --- a/src/readers/morphologySWC.cpp +++ b/src/readers/morphologySWC.cpp @@ -133,7 +133,7 @@ class SWCBuilder return somata; } - void raiseIfBrokenSoma(const Sample& sample) { + void raiseIfBrokenSoma(const Sample& sample, const unsigned int& options) { if (sample.type != SECTION_SOMA) { return; } @@ -149,7 +149,11 @@ class SWCBuilder } if (soma_bifurcations.size() > 1) { - throw morphio::SomaError(err.ERROR_SOMA_BIFURCATION(sample, soma_bifurcations)); + if (options & ALLOW_SOMA_BIFURCATIONS) { + printError(Warning::SOMA_BIFURCATION, err.WARNING_SOMA_BIFURCATION()); + } else { + throw morphio::SomaError(err.ERROR_SOMA_BIFURCATION(sample, soma_bifurcations)); + } } } @@ -171,11 +175,15 @@ class SWCBuilder } } - void checkSoma() { + void checkSoma(const unsigned int& options) { auto somata = _potentialSomata(); if (somata.size() > 1) { - throw morphio::SomaError(err.ERROR_MULTIPLE_SOMATA(somata)); + if (options & ALLOW_MULTIPLE_SOMATA) { + printError(Warning::MULTIPLE_SOMATA, err.WARNING_MULTIPLE_SOMATA()); + } else { + throw morphio::SomaError(err.ERROR_MULTIPLE_SOMATA(somata)); + } } if (somata.empty()) { @@ -241,9 +249,9 @@ class SWCBuilder return ret; } - void raiseIfNonConform(const Sample& sample) { + void raiseIfNonConform(const Sample& sample, const unsigned int& options) { raiseIfSelfParent(sample); - raiseIfBrokenSoma(sample); + raiseIfBrokenSoma(sample, options); raiseIfNoParent(sample); warnIfZeroDiameter(sample); } @@ -327,12 +335,29 @@ class SWCBuilder Property::Properties buildProperties(const std::string& contents, unsigned int options) { _readSamples(contents); + if (options & ALLOW_CUSTOM_ROOT_ID) { + for (auto& sample_pair : samples) { + auto& sample = sample_pair.second; + if (sample.parentId != SWC_ROOT && samples.count(sample.parentId) == 0) { + printError(Warning::CUSTOM_ROOT_ID, err.WARNING_CUSTOM_ROOT_ID(sample)); + // Remove the sample from the children + children[sample.parentId].erase(std::remove(children[sample.parentId].begin(), + children[sample.parentId].end(), + sample.id), + children[sample.parentId].end()); + // Update the sample and add it to the proper children + sample.parentId = SWC_ROOT; + children[SWC_ROOT].push_back(sample.id); + } + } + } + for (const auto& sample_pair : samples) { const auto& sample = sample_pair.second; - raiseIfNonConform(sample); + raiseIfNonConform(sample, options); } - checkSoma(); + checkSoma(options); // The process might occasionally creates empty section before // filling them so the warning is ignored @@ -345,7 +370,11 @@ class SWCBuilder // Bifurcation right at the start if (isRootPoint(sample) && isSectionEnd(sample)) { - continue; + if (options & ALLOW_ROOT_BIFURCATIONS) { + printError(Warning::ROOT_BIFURCATION, err.WARNING_ROOT_BIFURCATION(sample)); + } else { + continue; + } } if (isSectionStart(sample)) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4058b597f..ee89e7298 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,6 +7,7 @@ set(TESTS_SRC test_mutable_morphology.cpp test_point_utils.cpp test_properties.cpp + test_reader_options.cpp test_soma.cpp test_swc_reader.cpp test_utilities.cpp @@ -53,13 +54,13 @@ target_link_libraries(unittests ${TESTS_LINK_LIBRAIRIES} ) -add_test(NAME unittests - COMMAND unittests - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} - ) - if (NOT EXTERNAL_CATCH2) catch_discover_tests( unittests WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} ) +else() + add_test(NAME unittests + COMMAND unittests + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + ) endif() diff --git a/tests/data/edge_cases/bad_root_point.swc b/tests/data/edge_cases/bad_root_point.swc new file mode 100644 index 000000000..dd6492fed --- /dev/null +++ b/tests/data/edge_cases/bad_root_point.swc @@ -0,0 +1,7 @@ +# The root point is not -1 +1 2 0 0 0 6 0 +2 2 0 0 1 6 1 +3 2 0 0 2 6 2 +4 2 1 0 0 6 0 +5 2 1 0 1 6 1 +6 2 1 0 2 6 2 diff --git a/tests/data/edge_cases/root_bifurcation.asc b/tests/data/edge_cases/root_bifurcation.asc new file mode 100644 index 000000000..218e9eaff --- /dev/null +++ b/tests/data/edge_cases/root_bifurcation.asc @@ -0,0 +1,26 @@ +; The axon and the basal dendrite have root sections with only one point +("CellBody" + (Color Red) + (CellBody) + (0 0 0 6) + ) + + ((Axon) + (3 4 5 6) +) + + ((Dendrite) + (4 5 6 6) +) + + ((Dendrite) + (5 6 7 6) + (6 6 7 6) + (7 6 7 6) + ( + (8 6 7 6) + | + (9 6 7 6) + (10 6 7 6) + ) +) diff --git a/tests/data/edge_cases/root_bifurcation.h5 b/tests/data/edge_cases/root_bifurcation.h5 new file mode 100644 index 000000000..1fe544b63 Binary files /dev/null and b/tests/data/edge_cases/root_bifurcation.h5 differ diff --git a/tests/data/edge_cases/root_bifurcation.swc b/tests/data/edge_cases/root_bifurcation.swc new file mode 100644 index 000000000..fe78db337 --- /dev/null +++ b/tests/data/edge_cases/root_bifurcation.swc @@ -0,0 +1,10 @@ +# The axon and the basal dendrite have root sections with only one point +1 1 0 0 0 6 -1 +2 2 3 4 5 6 1 # one-point section +3 3 4 5 6 6 1 # one-point section +4 4 5 6 7 6 1 +5 4 6 6 7 6 4 +6 4 7 6 7 6 5 +7 4 8 6 7 6 6 # one-point section +8 4 9 6 7 6 6 +9 4 10 6 7 6 8 diff --git a/tests/data/edge_cases/soma_bifurcation.asc b/tests/data/edge_cases/soma_bifurcation.asc new file mode 100644 index 000000000..f005887dc --- /dev/null +++ b/tests/data/edge_cases/soma_bifurcation.asc @@ -0,0 +1,22 @@ +; The soma has a bifurcation +("CellBody" + (Color Red) + (CellBody) + (0 0 0 6) + (0 0 1 6) + ( + (0 0 2 6) + (0 0 3 6) + (0 0 4 6) + | + (0 0 5 6) + (0 0 6 6) + (0 0 7 6) + ) + ) + + ((Dendrite) + (0 0 3 6) + (0 1 3 6) + (0 2 3 6) +) diff --git a/tests/data/edge_cases/soma_bifurcation.h5 b/tests/data/edge_cases/soma_bifurcation.h5 new file mode 100644 index 000000000..1960a105e Binary files /dev/null and b/tests/data/edge_cases/soma_bifurcation.h5 differ diff --git a/tests/data/edge_cases/soma_bifurcation.swc b/tests/data/edge_cases/soma_bifurcation.swc new file mode 100644 index 000000000..a39c2df9d --- /dev/null +++ b/tests/data/edge_cases/soma_bifurcation.swc @@ -0,0 +1,11 @@ +# The soma has a bifurcation +1 1 0 0 0 6 -1 +2 1 0 0 1 6 1 +3 1 0 0 2 6 2 +4 1 0 0 3 6 3 +5 1 0 0 4 6 4 +6 1 0 0 5 6 2 +7 1 0 0 6 6 6 +8 1 0 0 7 6 7 +9 2 0 1 3 6 4 +10 2 0 2 3 6 9 diff --git a/tests/data/multiple_soma.asc b/tests/data/multiple_soma.asc new file mode 100644 index 000000000..83d5af7f0 --- /dev/null +++ b/tests/data/multiple_soma.asc @@ -0,0 +1,32 @@ +; Two somata +("CellBody" + (Color Red) + (CellBody) + (0 0 0 1) + ) + +("CellBody" + (Color Red) + (CellBody) + (1 0 0 1) + ) + + ((Axon) + (0 0 0 1) + (0 -4 0 1) + ( + (6 -4 0 1) + | + (-5 -4 0 1) + ) +) + + ((Dendrite) + (0 0 0 1) + (0 5 0 1) + ( + (-5 5 0 1) + | + (6 5 0 1) + ) +) diff --git a/tests/data/multiple_soma.h5 b/tests/data/multiple_soma.h5 new file mode 100644 index 000000000..651fff02c Binary files /dev/null and b/tests/data/multiple_soma.h5 differ diff --git a/tests/data/multiple_soma.swc b/tests/data/multiple_soma.swc index f223cf400..7557fe9de 100644 --- a/tests/data/multiple_soma.swc +++ b/tests/data/multiple_soma.swc @@ -1,11 +1,11 @@ # 2 soma, ids: 1, 10 - 1 1 0 0 0 1. -1 - 2 3 0 0 0 1. 1 - 3 3 0 5 0 1. 2 - 4 3 -5 5 0 1.5 3 - 5 3 6 5 0 1.5 3 - 6 2 0 0 0 1. 1 - 7 2 0 -4 0 1. 6 - 8 2 6 -4 0 2. 7 - 9 2 -5 -4 0 2. 7 - 10 1 0 0 0 1. -1 +1 1 0 0 0 1. -1 +2 3 0 0 0 1. 1 +3 3 0 5 0 1. 2 +4 3 -5 5 0 1. 3 +5 3 6 5 0 1. 3 +6 2 0 0 0 1. 1 +7 2 0 -4 0 1. 6 +8 2 6 -4 0 1. 7 +9 2 -5 -4 0 1. 7 +10 1 1 0 0 1. -1 diff --git a/tests/test_reader_options.cpp b/tests/test_reader_options.cpp new file mode 100644 index 000000000..1a50a4479 --- /dev/null +++ b/tests/test_reader_options.cpp @@ -0,0 +1,308 @@ +#include + +#include + +#include +#include + + +TEST_CASE("root bifurcation", "[loader_edge_cases]") { + std::vector exts{"asc", "h5", "swc"}; + for (auto&& ext : exts) { + SECTION("Testing mutable morphology with " + ext + " extension") { + auto mutable_morph_default_opts = morphio::mut::Morphology( + "data/edge_cases/root_bifurcation." + ext); + + if (ext == "swc") { + // By default, root bifurcations can be not loaded from SWC files + REQUIRE(mutable_morph_default_opts.rootSections().size() == 1); + REQUIRE(mutable_morph_default_opts.rootSections()[0]->points().size() == 3); + } else if (ext == "asc" || ext == "h5") { + // By default, root bifurcations can be loaded from ASC and H5 files + REQUIRE(mutable_morph_default_opts.rootSections().size() == 3); + REQUIRE(mutable_morph_default_opts.rootSections()[0]->points().size() == 1); + REQUIRE(mutable_morph_default_opts.rootSections()[1]->points().size() == 1); + REQUIRE(mutable_morph_default_opts.rootSections()[2]->points().size() == 3); + } else { + throw std::invalid_argument("Unknown extension: " + ext); + } + + auto mutable_morph = + morphio::mut::Morphology("data/edge_cases/root_bifurcation." + ext, + morphio::enums::Option::ALLOW_ROOT_BIFURCATIONS); + + REQUIRE(mutable_morph.rootSections().size() == 3); + REQUIRE(mutable_morph.rootSections()[0]->points().size() == 1); + REQUIRE(mutable_morph.rootSections()[1]->points().size() == 1); + REQUIRE(mutable_morph.rootSections()[2]->points().size() == 3); + } + + SECTION("Testing immutable morphology with " + ext + " extension") { + auto immutable_morph_default_opts = morphio::Morphology( + "data/edge_cases/root_bifurcation." + ext); + + if (ext == "swc") { + // By default, root bifurcations can be not loaded from SWC files + REQUIRE(immutable_morph_default_opts.rootSections().size() == 1); + REQUIRE(immutable_morph_default_opts.rootSections()[0].points().size() == 3); + } else if (ext == "asc" || ext == "h5") { + // By default, root bifurcations can be loaded from ASC and H5 files + REQUIRE(immutable_morph_default_opts.rootSections().size() == 3); + REQUIRE(immutable_morph_default_opts.rootSections()[0].points().size() == 1); + REQUIRE(immutable_morph_default_opts.rootSections()[1].points().size() == 1); + REQUIRE(immutable_morph_default_opts.rootSections()[2].points().size() == 3); + } else { + throw std::invalid_argument("Unknown extension: " + ext); + } + + auto immutable_morph = + morphio::Morphology("data/edge_cases/root_bifurcation." + ext, + morphio::enums::Option::ALLOW_ROOT_BIFURCATIONS); + + REQUIRE(immutable_morph.rootSections().size() == 3); + REQUIRE(immutable_morph.rootSections()[0].points().size() == 1); + REQUIRE(immutable_morph.rootSections()[1].points().size() == 1); + REQUIRE(immutable_morph.rootSections()[2].points().size() == 3); + } + } +} + + +TEST_CASE("soma bifurcation", "[loader_edge_cases]") { + std::vector exts{"asc", "swc"}; + for (auto&& ext : exts) { + SECTION("testing mutable morphology with " + ext + " extension") { + // By default, bifurcations in soma are considered as errors in ASC and SWC files + REQUIRE_THROWS(morphio::mut::Morphology("data/edge_cases/soma_bifurcation." + ext)); + + // Bifurcations in soma can optionaly be loaded from ASC and SWC files + auto mutable_morph = + morphio::mut::Morphology("data/edge_cases/soma_bifurcation." + ext, + morphio::enums::Option::ALLOW_SOMA_BIFURCATIONS); + + REQUIRE(mutable_morph.soma()->points().size() == 8); + REQUIRE(mutable_morph.rootSections().size() == 1); + } + + SECTION("testing immutable morphology with " + ext + " extension") { + // By default, bifurcations in soma are considered as errors in ASC and SWC files + REQUIRE_THROWS(morphio::Morphology("data/edge_cases/soma_bifurcation." + ext)); + + // Bifurcations in soma can optionaly be loaded from ASC and SWC files + auto immutable_morph = + morphio::Morphology("data/edge_cases/soma_bifurcation." + ext, + morphio::enums::Option::ALLOW_SOMA_BIFURCATIONS); + + REQUIRE(immutable_morph.soma().points().size() == 8); + REQUIRE(immutable_morph.rootSections().size() == 1); + } + } + SECTION("testing mutable morphology with h5 extension") { + // Bifurcations in soma are always considered as errors in H5 files + REQUIRE_THROWS(morphio::mut::Morphology("data/soma_bifurcation.h5")); + REQUIRE_THROWS(morphio::mut::Morphology("data/soma_bifurcation.h5", + morphio::enums::Option::ALLOW_SOMA_BIFURCATIONS)); + REQUIRE_THROWS(morphio::Morphology("data/soma_bifurcation.h5")); + REQUIRE_THROWS(morphio::Morphology("data/soma_bifurcation.h5", + morphio::enums::Option::ALLOW_SOMA_BIFURCATIONS)); + } +} + + +TEST_CASE("multiple somata", "[loader_edge_cases]") { + std::vector exts{"asc", "swc"}; + for (auto&& ext : exts) { + SECTION("testing mutable morphology with " + ext + " extension") { + // By default, multiple somata are considered as errors in ASC and SWC files + REQUIRE_THROWS(morphio::mut::Morphology("data/multiple_soma." + ext)); + + // Multiple somata can optionaly be loaded from ASC and SWC files + auto mutable_morph = + morphio::mut::Morphology("data/multiple_soma." + ext, + morphio::enums::Option::ALLOW_MULTIPLE_SOMATA); + + REQUIRE(mutable_morph.soma()->points().size() == 2); + REQUIRE(mutable_morph.rootSections().size() == 2); + } + + SECTION("testing immutable morphology with " + ext + " extension") { + // By default, multiple somata are considered as errors in ASC and SWC files + REQUIRE_THROWS(morphio::Morphology("data/multiple_soma." + ext)); + + // Multiple somata can optionaly be loaded from ASC and SWC files + auto immutable_morph = + morphio::Morphology("data/multiple_soma." + ext, + morphio::enums::Option::ALLOW_MULTIPLE_SOMATA); + + REQUIRE(immutable_morph.soma().points().size() == 2); + REQUIRE(immutable_morph.rootSections().size() == 2); + } + } + SECTION("testing mutable morphology with h5 extension") { + // Multiple somata are always considered as errors in H5 files + REQUIRE_THROWS(morphio::mut::Morphology("data/multiple_soma.h5")); + REQUIRE_THROWS(morphio::mut::Morphology("data/multiple_soma.h5", + morphio::enums::Option::ALLOW_MULTIPLE_SOMATA)); + REQUIRE_THROWS(morphio::Morphology("data/multiple_soma.h5")); + REQUIRE_THROWS(morphio::Morphology("data/multiple_soma.h5", + morphio::enums::Option::ALLOW_MULTIPLE_SOMATA)); + } +} + + +TEST_CASE("bad root point", "[loader_edge_cases]") { + std::vector exts{"swc"}; + for (auto&& ext : exts) { + SECTION("testing mutable morphology with " + ext + " extension") { + // By default, root points not equal to -1 are considered as errors in SWC files + REQUIRE_THROWS(morphio::mut::Morphology("data/edge_cases/bad_root_point." + ext)); + + // Bad root points can optionaly be loaded from SWC files + auto mutable_morph = + morphio::mut::Morphology("data/edge_cases/bad_root_point." + ext, + morphio::enums::Option::ALLOW_CUSTOM_ROOT_ID); + + REQUIRE(mutable_morph.soma()->points().size() == 0); + REQUIRE(mutable_morph.rootSections().size() == 2); + } + } +} + + +TEST_CASE("conversions", "[loader_edge_cases]") { + auto tmpDirectory = std::filesystem::temp_directory_path() / "test_reader_option_conversions"; + std::filesystem::create_directories(tmpDirectory); + + SECTION("testing SWC to other formats") { + SECTION("testing ALLOW_CUSTOM_ROOT_ID option") { + auto mutable_morph = + morphio::mut::Morphology("data/edge_cases/bad_root_point.swc", + morphio::enums::Option::ALLOW_CUSTOM_ROOT_ID); + + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_CUSTOM_ROOT_ID.asc"); + morphio::mut::Morphology(tmpDirectory / "from_swc_ALLOW_CUSTOM_ROOT_ID.asc"); + + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_CUSTOM_ROOT_ID.h5"); + morphio::mut::Morphology(tmpDirectory / "from_swc_ALLOW_CUSTOM_ROOT_ID.h5"); + + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_CUSTOM_ROOT_ID.swc"); + morphio::mut::Morphology(tmpDirectory / "from_swc_ALLOW_CUSTOM_ROOT_ID.swc"); + } + + SECTION("testing ALLOW_MULTIPLE_SOMATA option") { + auto mutable_morph = + morphio::mut::Morphology("data/multiple_soma.swc", + morphio::enums::Option::ALLOW_MULTIPLE_SOMATA); + + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_MULTIPLE_SOMATA.asc"); + morphio::mut::Morphology(tmpDirectory / "from_swc_ALLOW_MULTIPLE_SOMATA.asc"); + + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_MULTIPLE_SOMATA.h5"); + morphio::mut::Morphology(tmpDirectory / "from_swc_ALLOW_MULTIPLE_SOMATA.h5"); + + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_MULTIPLE_SOMATA.swc"); + morphio::mut::Morphology(tmpDirectory / "from_swc_ALLOW_MULTIPLE_SOMATA.swc"); + } + + SECTION("testing ALLOW_SOMA_BIFURCATIONS option") { + auto mutable_morph = + morphio::mut::Morphology("data/edge_cases/soma_bifurcation.swc", + morphio::enums::Option::ALLOW_SOMA_BIFURCATIONS); + + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_SOMA_BIFURCATIONS.asc"); + morphio::mut::Morphology(tmpDirectory / "from_swc_ALLOW_SOMA_BIFURCATIONS.asc"); + + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_SOMA_BIFURCATIONS.h5"); + morphio::mut::Morphology(tmpDirectory / "from_swc_ALLOW_SOMA_BIFURCATIONS.h5"); + + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_SOMA_BIFURCATIONS.swc"); + morphio::mut::Morphology(tmpDirectory / "from_swc_ALLOW_SOMA_BIFURCATIONS.swc"); + } + + SECTION("testing ALLOW_ROOT_BIFURCATIONS option") { + auto mutable_morph = + morphio::mut::Morphology("data/edge_cases/root_bifurcation.swc", + morphio::enums::Option::ALLOW_ROOT_BIFURCATIONS); + REQUIRE_THROWS( + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_ROOT_BIFURCATIONS.asc")); + REQUIRE_THROWS( + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_ROOT_BIFURCATIONS.h5")); + REQUIRE_THROWS( + mutable_morph.write(tmpDirectory / "from_swc_ALLOW_ROOT_BIFURCATIONS.swc")); + } + } + + SECTION("testing ASC to other formats") { + SECTION("testing ALLOW_CUSTOM_ROOT_ID option") { + // Can not be loaded with ASC format + } + + SECTION("testing ALLOW_MULTIPLE_SOMATA option") { + auto mutable_morph = + morphio::mut::Morphology("data/multiple_soma.asc", + morphio::enums::Option::ALLOW_MULTIPLE_SOMATA); + + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_MULTIPLE_SOMATA.asc"); + morphio::mut::Morphology(tmpDirectory / "from_asc_ALLOW_MULTIPLE_SOMATA.asc"); + + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_MULTIPLE_SOMATA.h5"); + morphio::mut::Morphology(tmpDirectory / "from_asc_ALLOW_MULTIPLE_SOMATA.h5"); + + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_MULTIPLE_SOMATA.swc"); + morphio::mut::Morphology(tmpDirectory / "from_asc_ALLOW_MULTIPLE_SOMATA.swc"); + } + + SECTION("testing ALLOW_SOMA_BIFURCATIONS option") { + auto mutable_morph = + morphio::mut::Morphology("data/edge_cases/soma_bifurcation.asc", + morphio::enums::Option::ALLOW_SOMA_BIFURCATIONS); + + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_SOMA_BIFURCATIONS.asc"); + morphio::mut::Morphology(tmpDirectory / "from_asc_ALLOW_SOMA_BIFURCATIONS.asc"); + + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_SOMA_BIFURCATIONS.h5"); + morphio::mut::Morphology(tmpDirectory / "from_asc_ALLOW_SOMA_BIFURCATIONS.h5"); + + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_SOMA_BIFURCATIONS.swc"); + morphio::mut::Morphology(tmpDirectory / "from_asc_ALLOW_SOMA_BIFURCATIONS.swc"); + } + + SECTION("testing ALLOW_ROOT_BIFURCATIONS option") { + auto mutable_morph = + morphio::mut::Morphology("data/edge_cases/root_bifurcation.asc", + morphio::enums::Option::ALLOW_ROOT_BIFURCATIONS); + REQUIRE_THROWS( + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_ROOT_BIFURCATIONS.asc")); + REQUIRE_THROWS( + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_ROOT_BIFURCATIONS.h5")); + REQUIRE_THROWS( + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_ROOT_BIFURCATIONS.swc")); + } + } + + SECTION("testing H5 to other formats") { + SECTION("testing ALLOW_CUSTOM_ROOT_ID option") { + // Can not be loaded with H5 format + } + + SECTION("testing ALLOW_MULTIPLE_SOMATA option") { + // Can not be loaded with H5 format + } + + SECTION("testing ALLOW_SOMA_BIFURCATIONS option") { + // Can not be loaded with H5 format + } + + SECTION("testing ALLOW_ROOT_BIFURCATIONS option") { + auto mutable_morph = + morphio::mut::Morphology("data/edge_cases/root_bifurcation.h5", + morphio::enums::Option::ALLOW_ROOT_BIFURCATIONS); + REQUIRE_THROWS( + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_ROOT_BIFURCATIONS.asc")); + REQUIRE_THROWS( + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_ROOT_BIFURCATIONS.h5")); + REQUIRE_THROWS( + mutable_morph.write(tmpDirectory / "from_asc_ALLOW_ROOT_BIFURCATIONS.swc")); + } + } +}