• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

BlueBrain / MorphIO / 6533524408

16 Oct 2023 12:24PM UTC coverage: 76.051% (-0.02%) from 76.073%
6533524408

push

github

mgeplf
fix test_root_node_split

1972 of 2593 relevant lines covered (76.05%)

903.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.16
/src/readers/morphologyASC.cpp
1
/* Copyright (c) 2013-2023, EPFL/Blue Brain Project
2
 *
3
 * SPDX-License-Identifier: Apache-2.0
4
 */
5

6
#include "morphologyASC.h"
7
#include "utils.h"
8

9
#include <morphio/mut/morphology.h>
10
#include <morphio/mut/section.h>
11

12

13
#include "NeurolucidaLexer.inc"
14

15
namespace morphio {
16
namespace readers {
17
namespace asc {
18
namespace {
19

20
/**
21
   Contain header info about the root S-exps
22
**/
23
struct Header {
24
    Token token = static_cast<Token>(+Token::STRING);
25
    std::string label;
26
    int32_t parent_id = -1;
27
};
28

29
bool is_eof(Token type) {
1,296✔
30
    return type == Token::EOF_;
1,296✔
31
}
32

33
bool is_end_of_branch(Token type) {
602✔
34
    return (type == Token::GENERATED || type == Token::HIGH || type == Token::INCOMPLETE ||
602✔
35
            type == Token::LOW || type == Token::NORMAL || type == Token::MIDPOINT ||
1,204✔
36
            type == Token::ORIGIN);
602✔
37
}
38

39
bool is_neurite_type(Token id) {
264✔
40
    return (id == Token::AXON || id == Token::APICAL || id == Token::DENDRITE ||
264✔
41
            id == Token::CELLBODY);
264✔
42
}
43

44
std::string text_to_uppercase_token_string(const std::string& text) {
46✔
45
    std::string upp_text = text;
46✔
46
    std::transform(text.begin(), text.end(), upp_text.begin(), ::toupper);
46✔
47
    const auto end_pos = std::remove(upp_text.begin(), upp_text.end(), ' ');
46✔
48
    upp_text.erase(end_pos, upp_text.end());
46✔
49
    return upp_text;
92✔
50
}
51

52
bool is_end_of_section(Token id) {
926✔
53
    return (id == Token::RPAREN || id == Token::PIPE);
926✔
54
}
55

56
bool skip_sexp(size_t id) {
922✔
57
    return (id == +Token::WORD || id == +Token::COLOR || id == +Token::GENERATED ||
2,668✔
58
            id == +Token::HIGH || id == +Token::INCOMPLETE || id == +Token::LOW ||
2,526✔
59
            id == +Token::NORMAL || id == +Token::FONT);
2,668✔
60
}
61

62
class NeurolucidaParser
63
{
64
  public:
65
    explicit NeurolucidaParser(const std::string& uri)
50✔
66
        : uri_(uri)
50✔
67
        , lex_(uri, false)
68
        , err_(uri) {}
50✔
69

70
    NeurolucidaParser(NeurolucidaParser const&) = delete;
71
    NeurolucidaParser& operator=(NeurolucidaParser const&) = delete;
72

73
    morphio::mut::Morphology& parse(const std::string& input) {
50✔
74
        lex_.start_parse(input);
50✔
75
        parse_root_sexps();
50✔
76
        return nb_;
48✔
77
    }
78

79
  private:
80
    std::tuple<Point, floatType> parse_point(NeurolucidaLexer& lex, bool is_marker) {
476✔
81
        lex.expect(Token::LPAREN, "Point should start in LPAREN");
476✔
82
        std::array<morphio::floatType, 4> point{};  // X,Y,Z,D
476✔
83
        const auto& stn = morphio::getStringToNumber();
476✔
84

85
        for (unsigned int i = 0; i < 4; i++) {
2,380✔
86
            const std::string s = lex.consume()->str();
1,904✔
87
            try {
88
                point[i] = std::get<0>(stn.toFloat(s, 0));
1,904✔
89
            } catch (const std::invalid_argument&) {
×
90
                throw RawDataError(err_.ERROR_PARSING_POINT(lex.line_num(), s));
×
91
            }
92

93
            // Markers can have an s-exp (X Y Z) without diameter
94
            if (is_marker && i == 2 && (lex_.peek()->str() == ")")) {
1,904✔
95
                point[3] = 0;
×
96
                break;
×
97
            }
98
        }
99

100
        lex.consume();
476✔
101

102
        // case where the s-exp is (X Y Z R WORD). For example: (1 1 0 1 S1)
103
        if (lex.current()->id == +Token::WORD) {
476✔
104
            lex.consume(Token::WORD);
×
105
        }
106

107
        lex.consume(Token::RPAREN, "Point should end in RPAREN");
476✔
108

109
        return {{point[0], point[1], point[2]}, point[3]};
476✔
110
    }
111

112
    bool parse_neurite_branch(Header& header) {
94✔
113
        lex_.consume(Token::LPAREN, "New branch should start with LPAREN");
94✔
114

115
        bool ret = true;
94✔
116
        while (true) {
117
            ret &= parse_neurite_section(header);
180✔
118
            if (lex_.ended() ||
448✔
119
                (lex_.current()->id != +Token::PIPE && lex_.current()->id != +Token::LPAREN)) {
270✔
120
                break;
92✔
121
            }
122
            lex_.consume();
86✔
123
        }
124
        lex_.consume(Token::RPAREN, "Branch should end with RPAREN");
92✔
125
        return ret;
92✔
126
    }
127

128
    int32_t _create_soma_or_section(const Header& header,
318✔
129
                                    std::vector<Point>& points,
130
                                    std::vector<morphio::floatType>& diameters) {
131
        int32_t return_id = -1;
318✔
132
        morphio::Property::PointLevel properties;
318✔
133
        properties._points = points;
318✔
134
        properties._diameters = diameters;
318✔
135

136
        if (header.token == Token::STRING) {
318✔
137
            Property::Marker marker;
12✔
138
            marker._pointLevel = properties;
12✔
139
            marker._label = header.label;
12✔
140
            marker._sectionId = header.parent_id;
12✔
141
            nb_.addMarker(marker);
12✔
142
            return_id = -1;
12✔
143
        } else if (header.token == Token::CELLBODY) {
306✔
144
            if (!nb_.soma()->points().empty())
42✔
145
                throw SomaError(err_.ERROR_SOMA_ALREADY_DEFINED(lex_.line_num()));
×
146
            nb_.soma()->properties() = properties;
42✔
147
            return_id = -1;
42✔
148
        } else {
149
            SectionType section_type = TokenToSectionType(header.token);
264✔
150
            insertLastPointParentSection(header.parent_id, properties, diameters);
264✔
151

152
            // Condition to remove single point section that duplicate parent
153
            // point See test_single_point_section_duplicate_parent for an
154
            // example
155
            if (header.parent_id > -1 && properties._points.size() == 1) {
264✔
156
                return_id = header.parent_id;
×
157
            } else {
158
                std::shared_ptr<morphio::mut::Section> section;
×
159
                if (header.parent_id > -1)
264✔
160
                    section = nb_.section(static_cast<unsigned int>(header.parent_id))
176✔
161
                                  ->appendSection(properties, section_type);
176✔
162
                else
163
                    section = nb_.appendRootSection(properties, section_type);
88✔
164
                return_id = static_cast<int>(section->id());
264✔
165
            }
166
        }
167
        points.clear();
318✔
168
        diameters.clear();
318✔
169

170
        return return_id;
636✔
171
    }
172

173
    /*
174
      Add the last point of parent section to the beginning of this section
175
      if not already present.
176
      See https://github.com/BlueBrain/MorphIO/pull/221
177

178
      The diameter is taken from the child section next point as does NEURON.
179
      Here is the spec:
180
      https://bbpteam.epfl.ch/project/issues/browse/NSETM-1178?focusedCommentId=135030&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-135030
181

182
      In term of diameters, the result should look like the right picture of:
183
      https://github.com/BlueBrain/NeuroM/issues/654#issuecomment-332864540
184

185
     The idea is that these two structures should represent the same morphology:
186

187
     (3 -8 0 5)     and          (3 -8 0 5)
188
     (3 -10 0 5)                 (3 -10 0 5)
189
     (                           (
190
       (0 -10 0 2)                 (3 -10 0 2)  <-- duplicate parent point, note that the
191
       (-3 -10 0 2)                (0 -10 0 2)      diameter is 2 and not 5
192
       |                           (-3 -10 0 2)
193
       (6 -10 0 2)                 |
194
       (9 -10 0 2)                 (3 -10 0 2)  <-- duplicate parent point, note that the
195
     )                             (6 -10 0 2)      diameter is 2 and not 5
196
                                   (9 -10 0 2)
197
                                 )
198
     */
199
    void insertLastPointParentSection(int32_t parentId,
264✔
200
                                      morphio::Property::PointLevel& properties,
201
                                      std::vector<morphio::floatType>& diameters) {
202
        if (parentId < 0)  // Discard root sections
264✔
203
            return;
100✔
204
        auto parent = nb_.section(static_cast<unsigned int>(parentId));
176✔
205
        auto lastParentPoint = parent->points()[parent->points().size() - 1];
176✔
206
        auto childSectionNextDiameter = diameters[0];
176✔
207

208
        if (lastParentPoint == properties._points[0])
176✔
209
            return;
12✔
210

211
        properties._points.insert(properties._points.begin(), lastParentPoint);
164✔
212
        properties._diameters.insert(properties._diameters.begin(), childSectionNextDiameter);
164✔
213
    }
214

215
    /**
216
       Parse the root sexp until finding the first sexp containing numbers
217
    **/
218
    Header parse_root_sexp_header() {
134✔
219
        Header header;
134✔
220

221
        while (true) {
222
            const Token id = static_cast<Token>(lex_.current()->id);
376✔
223
            const size_t peek_id = lex_.peek()->id;
376✔
224

225
            if (is_eof(id)) {
376✔
226
                throw RawDataError(err_.ERROR_EOF_IN_NEURITE(lex_.line_num()));
×
227
            } else if (id == Token::MARKER) {
376✔
228
                lex_.consume();
×
229
            } else if (id == Token::WORD) {
376✔
230
                lex_.consume_until_balanced_paren();
2✔
231
                lex_.consume(Token::LPAREN);
2✔
232
            } else if (id == Token::STRING) {
374✔
233
                header.label = lex_.current()->str();
46✔
234
                // Get rid of quotes
235
                header.label = header.label.substr(1, header.label.size() - 2);
46✔
236

237
                // Early NeuroLucida files contained the soma in a named String
238
                // s-exp: https://github.com/BlueBrain/MorphIO/issues/300
239
                const std::string uppercase_label = text_to_uppercase_token_string(header.label);
46✔
240
                if (uppercase_label == "CELLBODY") {
46✔
241
                    header.token = Token::CELLBODY;
42✔
242
                }
243

244
                lex_.consume();
46✔
245
            } else if (id == Token::RPAREN) {
328✔
246
                return header;
×
247
            } else if (id == Token::LPAREN) {
328✔
248
                const auto next_token = static_cast<Token>(peek_id);
328✔
249
                if (skip_sexp(peek_id)) {
328✔
250
                    // skip words/strings/markers
251
                    lex_.consume_until_balanced_paren();
64✔
252
                    if (peek_id == +Token::FONT)
64✔
253
                        lex_.consume_until_balanced_paren();
×
254
                } else if (is_neurite_type(next_token)) {
264✔
255
                    header.token = next_token;
130✔
256
                    lex_.consume();  // Advance to NeuriteType
130✔
257
                    lex_.consume();
130✔
258
                    lex_.consume(Token::RPAREN, "New Neurite should end in RPAREN");
130✔
259
                } else if (peek_id == +Token::NUMBER) {
134✔
260
                    return header;
134✔
261
                } else {
262
                    throw RawDataError(
263
                        err_.ERROR_UNKNOWN_TOKEN(lex_.line_num(), lex_.peek()->str()));
×
264
                }
265
            } else {
266
                throw RawDataError(
267
                    err_.ERROR_UNKNOWN_TOKEN(lex_.line_num(), lex_.current()->str()));
×
268
            }
269
        }
242✔
270
    }
271

272

273
    bool parse_neurite_section(const Header& header) {
322✔
274
        Points points;
644✔
275
        std::vector<morphio::floatType> diameters;
326✔
276
        auto section_id = static_cast<int>(nb_.sections().size());
322✔
277

278
        while (true) {
279
            const auto id = static_cast<Token>(lex_.current()->id);
920✔
280
            const size_t peek_id = lex_.peek()->id;
920✔
281

282
            if (is_eof(id)) {
920✔
283
                throw RawDataError(err_.ERROR_EOF_IN_NEURITE(lex_.line_num()));
×
284
            } else if (is_end_of_section(id)) {
920✔
285
                if (!points.empty()) {
318✔
286
                    _create_soma_or_section(header, points, diameters);
224✔
287
                }
288
                return true;
636✔
289
            } else if (is_end_of_branch(id)) {
602✔
290
                if (id == Token::INCOMPLETE) {
8✔
291
                    Property::Marker marker;
12✔
292
                    marker._label = to_string(Token::INCOMPLETE);
6✔
293
                    marker._sectionId = section_id;
6✔
294
                    nb_.addMarker(marker);
6✔
295
                    if (!is_end_of_section(Token(peek_id))) {
6✔
296
                        throw RawDataError(err_.ERROR_UNEXPECTED_TOKEN(
8✔
297
                            lex_.line_num(),
298
                            lex_.peek()->str(),
4✔
299
                            lex_.current()->str(),
4✔
300
                            "'Incomplete' tag must finish the branch."));
6✔
301
                    }
302
                }
303
                lex_.consume();
6✔
304
            } else if (id == Token::LSPINE) {
594✔
305
                // skip spines
306
                while (!lex_.ended() && static_cast<Token>(lex_.current()->id) != Token::RSPINE) {
×
307
                    lex_.consume();
×
308
                }
309
                lex_.consume(Token::RSPINE, "Must be end of spine");
×
310
            } else if (id == Token::LPAREN) {
594✔
311
                if (skip_sexp(peek_id)) {
594✔
312
                    // skip words/strings
313
                    lex_.consume_until_balanced_paren();
16✔
314
                } else if (peek_id == +Token::MARKER) {
578✔
315
                    Header marker_header;
8✔
316
                    marker_header.parent_id = section_id;
8✔
317
                    marker_header.token = Token::STRING;
8✔
318
                    marker_header.label = lex_.peek()->str();
8✔
319
                    lex_.consume_until(Token::LPAREN);
8✔
320
                    parse_neurite_section(marker_header);
8✔
321
                    lex_.consume(Token::RPAREN, "Marker should end with RPAREN");
8✔
322
                } else if (peek_id == +Token::NUMBER) {
570✔
323
                    Point point;
324
                    floatType radius;
325
                    std::tie(point, radius) = parse_point(lex_, (header.token == Token::STRING));
476✔
326
                    points.push_back(point);
476✔
327
                    diameters.push_back(radius);
476✔
328
                } else if (peek_id == +Token::LPAREN) {
94✔
329
                    if (!points.empty()) {
94✔
330
                        section_id = _create_soma_or_section(header, points, diameters);
94✔
331
                    }
332
                    Header child_header = header;
188✔
333
                    child_header.parent_id = section_id;
94✔
334
                    parse_neurite_branch(child_header);
94✔
335
                } else {
336
                    throw RawDataError(
337
                        err_.ERROR_UNKNOWN_TOKEN(lex_.line_num(), lex_.peek()->str()));
×
338
                }
339
            } else if (id == Token::STRING) {
×
340
                lex_.consume();
×
341
            } else {
342
                throw RawDataError(err_.ERROR_UNKNOWN_TOKEN(lex_.line_num(), lex_.peek()->str()));
×
343
            }
344
        }
598✔
345
    }
346

347
    void parse_root_sexps() {
220✔
348
        // parse the top level blocks, and if they are a neurite, otherwise skip
349
        while (!lex_.ended()) {
220✔
350
            if (static_cast<Token>(lex_.current()->id) == Token::LPAREN) {
172✔
351
                lex_.consume();
134✔
352
                const Header header = parse_root_sexp_header();
268✔
353
                if (lex_.current()->id != +Token::RPAREN) {
134✔
354
                    parse_neurite_section(header);
134✔
355
                }
356
            }
357

358
            if (!lex_.ended())
170✔
359
                lex_.consume();
170✔
360
        }
361
    }
48✔
362

363
    morphio::mut::Morphology nb_;
364

365
    std::string uri_;
366
    NeurolucidaLexer lex_;
367

368
    ErrorMessages err_;
369
};
370

371
}  // namespace
372

373
Property::Properties load(const std::string& path,
50✔
374
                          const std::string& contents,
375
                          unsigned int options) {
376
    NeurolucidaParser parser(path);
100✔
377

378
    morphio::mut::Morphology& nb_ = parser.parse(contents);
50✔
379
    nb_.applyModifiers(options);
48✔
380

381
    Property::Properties properties = nb_.buildReadOnly();
48✔
382
    properties._cellLevel._cellFamily = NEURON;
48✔
383
    properties._cellLevel._version = {"asc", 1, 0};
48✔
384
    return properties;
96✔
385
}
386

387
}  // namespace asc
388
}  // namespace readers
389
}  // namespace morphio
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc