diff --git a/include/morphio/errorMessages.h b/include/morphio/errorMessages.h index 28d79fe6f..279a00b0e 100644 --- a/include/morphio/errorMessages.h +++ b/include/morphio/errorMessages.h @@ -141,8 +141,7 @@ class ErrorMessages { //////////////////////////////////////////////////////////////////////////////// // NEUROLUCIDA //////////////////////////////////////////////////////////////////////////////// - const std::string ERROR_SOMA_ALREADY_DEFINED(int lineNumber) const; - + const std::string ERROR_MULTIPLE_SOMA_STACKS_WITH_SAME_Z(int line1, int line2, float z) const; const std::string ERROR_PARSING_POINT(int lineNumber, const std::string& point) const; diff --git a/src/errorMessages.cpp b/src/errorMessages.cpp index 49adcd900..80ceacfe2 100644 --- a/src/errorMessages.cpp +++ b/src/errorMessages.cpp @@ -143,9 +143,15 @@ const std::string _col(float val1, float val2) { //////////////////////////////////////////////////////////////////////////////// // NEUROLUCIDA //////////////////////////////////////////////////////////////////////////////// -const std::string ErrorMessages::ERROR_SOMA_ALREADY_DEFINED(int lineNumber) const { - return errorMsg(lineNumber, ErrorLevel::ERROR, - "A soma is already defined"); +const std::string ErrorMessages::ERROR_MULTIPLE_SOMA_STACKS_WITH_SAME_Z(int line1, int line2, float z) const { + std::string msg = errorMsg(line1, ErrorLevel::ERROR, + "\nThere is already a soma stack for Z == "+std::to_string(z) + " at line:"); + msg += errorMsg(line2, ErrorLevel::INFO, ""); + msg += "\nNote: Multiple blocks of type CellBody means the soma is represented as a soma stack.\n" + "(More info on soma stacks: https://www.neuron.yale.edu/phpBB/viewtopic.php?t=3833)\n" + "In this case, each CellBody block is the soma contour for a given Z position\n" + " and each block must have a different Z value"; + return msg; } diff --git a/src/plugin/morphologyASC.cpp b/src/plugin/morphologyASC.cpp index 648109e44..df5bb2855 100644 --- a/src/plugin/morphologyASC.cpp +++ b/src/plugin/morphologyASC.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include @@ -71,12 +73,44 @@ class NeurolucidaParser parse_block(); + create_soma(); + return nb_; } + + void create_soma() { + if(somaStacks.size() == 1) { + nb_.soma() -> properties() = somaStacks[0]; + } else if(somaStacks.size() > 1) { + _raiseIfEachStackHasMultipleZ(); + for (const auto& stack: somaStacks) { + morphio::Property::_appendVector(nb_.soma() -> properties()._points, stack._points, 0); + morphio::Property::_appendVector(nb_.soma() -> properties()._diameters, stack._diameters, 0); + } + } + } + private: + void _raiseIfEachStackHasMultipleZ() { + for (const auto& stack: somaStacks) { + std::unordered_set level; + for(const Point& point: stack._points) { + level.insert(point[2]); + } + + if (level.size() > 1) { + std::string msg("Soma stack has multiple Z levels:\n"); + for (float z: level) + msg += " * " + std::to_string(z) + "\n"; + + throw RawDataError(msg); + } + } + } + std::tuple parse_point(NeurolucidaLexer& lex) { lex.expect(Token::LPAREN, "Point should start in LPAREN"); @@ -125,16 +159,19 @@ class NeurolucidaParser int32_t _create_soma_or_section(Token token, int32_t parent_id, std::vector &points, std::vector &diameters) { - lex_.current_section_start_ = lex_.line_num(); int32_t return_id; morphio::Property::PointLevel properties; properties._points = points; properties._diameters = diameters; if(token == Token::CELLBODY){ - if(nb_.soma()->points().size() != 0) - throw SomaError(err_.ERROR_SOMA_ALREADY_DEFINED(lex_.line_num())); - nb_.soma() -> properties() = properties; - + float z = points[0][2]; + if (soma_z_levels.count(z)) + throw RawDataError(err_.ERROR_MULTIPLE_SOMA_STACKS_WITH_SAME_Z(lex_.current_section_start_, + soma_z_levels[z], + z)); + soma_z_levels[z] = lex_.current_section_start_; + + somaStacks.push_back(properties); return_id = -1; } else { SectionType section_type = TokenSectionTypeMap.at(token); @@ -199,6 +236,8 @@ class NeurolucidaParser Points points; std::vector diameters; uint32_t section_id = nb_.sections().size(); + lex_.current_section_start_ = lex_.line_num(); + while (true) { @@ -297,7 +336,8 @@ class NeurolucidaParser DebugInfo debugInfo_; private: ErrorMessages err_; - + std::vector somaStacks; + std::unordered_map soma_z_levels; }; Property::Properties load(const URI& uri, unsigned int options) diff --git a/tests/test_2_neurolucida.py b/tests/test_2_neurolucida.py index 312becba5..570a21438 100644 --- a/tests/test_2_neurolucida.py +++ b/tests/test_2_neurolucida.py @@ -55,26 +55,6 @@ def test_unfinished_point(): ':4:error') -def test_multiple_soma(): - _test_asc_exception(''' - ("CellBody" - (Color Red) - (CellBody) - (1 1 0 1 S1) - (-1 1 0 1 S2) - ) - - ("CellBody" - (Color Red) - (CellBody) - (1 1 0 1 S1) - (-1 1 0 1 S2) - )''', - SomaError, - 'A soma is already defined', - ':14:error') - - def test_single_neurite_no_soma(): with tmp_asc_file(''' ( (Color Yellow) @@ -516,3 +496,74 @@ def test_markers(): [-268.17, -130.62, -24.75], [-266.79, -131.77, -26.13]], dtype=np.float32)) + + +def test_multiple_soma_stack(): + _test_asc_exception(''' +( + (CellBody) + ( 5.55 5.05 1.0 2.35) + ( 2.86 5.72 1.0 2.35) +) + +( + (CellBody) + ( 0.84 1.52 2.0 2.35) + ( -4.54 2.36 2.0 2.35) + ( -6.55 0.17 10.0 2.35) ; Wrong Z +) +''', + RawDataError, + "Soma stack has multiple Z levels:", +''' * 10.000000 + * 2.000000''' +) + + _test_asc_exception(''' +( + (CellBody) + ( 5.88 0.84 1.0 2.35) + ( 6.05 2.53 1.0 2.35) + ( 6.39 4.38 1.0 2.35) +) + +( + (CellBody) + ( 1.85 0.67 1.0 2.35) + ( 0.84 1.52 1.0 2.35) + ( -4.54 2.36 1.0 2.35) +) +''', + RawDataError, + ':11:error', + 'There is already a soma stack for Z == 1.000000 at line:', + ':4:info' +) + + with tmp_asc_file(''' +( + (CellBody) + ( 5.88 0.84 1.0 2.35) + ( 6.05 2.53 1.0 2.35) + ( 6.39 4.38 1.0 2.35) +) + +( + (CellBody) + ( 1.85 0.67 2.0 2.35) + ( 0.84 1.52 2.0 2.35) + ( -4.54 2.36 2.0 2.35) +) +''') as tmp_file: + m = Morphology(tmp_file.name) + assert_array_equal(m.soma.points, + np.array([[ 5.88, 0.84, 1. ], + [ 6.05, 2.53, 1. ], + [ 6.39, 4.38, 1. ], + [ 1.85, 0.67, 2. ], + [ 0.84, 1.52, 2. ], + [-4.54, 2.36, 2. ]], + dtype=np.float32)) + assert_array_equal(m.soma.diameters, + np.array([2.35] * 6, + dtype=np.float32)) diff --git a/tests/utils.py b/tests/utils.py index 1bcff938a..384e23ee4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -66,17 +66,24 @@ def assert_string_equal(str1, str2): str1.join(sep), str2.join(sep))) -def _test_exception(content, exception, str1, str2, extension): - '''Create tempfile with given content and check that the exception is raised''' - with _tmp_file(content, extension) as tmp_file: +def _test_swc_exception(content, exception, *messages): + '''Create tempfile with given content, check that the exception is raised + and that messages are part of the error message''' + with _tmp_file(content, 'swc') as tmp_file: with assert_raises(exception) as obj: Morphology(tmp_file.name) - assert_substring(str1, str(obj.exception)) - assert_substring(str2, str(obj.exception)) + for msg in messages: + assert_substring(msg, str(obj.exception)) +def _test_asc_exception(content, exception, *messages): + '''Create tempfile with given content, check that the exception is raised + and that messages are part of the error message''' + with _tmp_file(content, 'asc') as tmp_file: + with assert_raises(exception) as obj: + Morphology(tmp_file.name) + for msg in messages: + assert_substring(msg, str(obj.exception)) -_test_asc_exception = partial(_test_exception, extension='asc') -_test_swc_exception = partial(_test_exception, extension='swc') @contextmanager