Skip to content

Commit

Permalink
WIP: Add options to disable some checks
Browse files Browse the repository at this point in the history
Add soma bifurcation for ASC files

Add new options and tests

Use proper warnings and fix tests

Rename single point section into root bifurcation

Add bindings

Cleaning

Clean tests

Apply clang-format

Improve readability of enum Option

Remove duplicated test

Restore add_test in else() block

Add option for root point not equal to -1 in SWC files

Add Python bindings for custom root id

Add tests for conversions

Check that converted files can be read
  • Loading branch information
adrien-berchet committed Sep 29, 2023
1 parent 89fcfe3 commit 4296eb9
Show file tree
Hide file tree
Showing 17 changed files with 550 additions and 49 deletions.
18 changes: 13 additions & 5 deletions include/morphio/enums.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

/**
Expand All @@ -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 {
Expand Down
13 changes: 11 additions & 2 deletions include/morphio/errorMessages.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -244,7 +252,8 @@ class ErrorMessages
std::string WARNING_WRONG_DUPLICATE(const std::shared_ptr<morphio::mut::Section>& current,
const std::shared_ptr<morphio::mut::Section>& parent) const;
/** Writing empty section warning message */
std::string WARNING_APPENDING_EMPTY_SECTION(std::shared_ptr<morphio::mut::Section>);
std::string WARNING_APPENDING_EMPTY_SECTION(
const std::shared_ptr<morphio::mut::Section>& section) const;
/** Writing single child section warning message */
std::string WARNING_ONLY_CHILD(const DebugInfo& info,
unsigned int parentId,
Expand All @@ -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<Sample>& children) const;
Expand Down
26 changes: 24 additions & 2 deletions src/errorMessages.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -307,12 +315,26 @@ std::string ErrorMessages::WARNING_DISCONNECTED_NEURITE(const Sample& sample) co
}

std::string ErrorMessages::WARNING_APPENDING_EMPTY_SECTION(
std::shared_ptr<morphio::mut::Section> section) {
const std::shared_ptr<morphio::mut::Section>& 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<morphio::mut::Section>& current,
const std::shared_ptr<morphio::mut::Section>& parent) const {
Expand Down Expand Up @@ -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"
Expand Down
48 changes: 32 additions & 16 deletions src/readers/morphologyASC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
}

Expand Down Expand Up @@ -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;
Expand All @@ -130,7 +130,8 @@ class NeurolucidaParser

int32_t _create_soma_or_section(const Header& header,
std::vector<Point>& points,
std::vector<morphio::floatType>& diameters) {
std::vector<morphio::floatType>& diameters,
unsigned int& options) {
int32_t return_id = -1;
morphio::Property::PointLevel properties;
properties._points = points;
Expand All @@ -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);
Expand Down Expand Up @@ -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<morphio::floatType> diameters;
auto section_id = static_cast<int>(nb_.sections().size());
Expand All @@ -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)) {
Expand Down Expand Up @@ -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;
Expand All @@ -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()));
Expand All @@ -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<Token>(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);
}
}

Expand All @@ -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();
Expand Down
47 changes: 38 additions & 9 deletions src/readers/morphologySWC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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));
}
}
}

Expand All @@ -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()) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand All @@ -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)) {
Expand Down
11 changes: 6 additions & 5 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
7 changes: 7 additions & 0 deletions tests/data/edge_cases/bad_root_point.swc
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 4296eb9

Please sign in to comment.