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

BlueBrain / MorphIO / 11255477262

09 Oct 2024 12:28PM UTC coverage: 77.697% (+0.08%) from 77.615%
11255477262

Pull #476

github

mgeplf
comments
Pull Request #476: Efficient swc build

286 of 321 new or added lines in 10 files covered. (89.1%)

9 existing lines in 4 files now uncovered.

2125 of 2735 relevant lines covered (77.7%)

901.01 hits per line

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

87.5
/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 "../error_message_generation.h"
14
#include "NeurolucidaLexer.inc"
15
#include "morphio/enums.h"
16

17
namespace morphio {
18
namespace readers {
19
namespace asc {
20
namespace {
21

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

31
bool is_eof(Token type) {
1,474✔
32
    return type == Token::EOF_;
1,474✔
33
}
34

35
bool is_end_of_branch(Token type) {
750✔
36
    return (type == Token::GENERATED || type == Token::HIGH || type == Token::INCOMPLETE ||
750✔
37
            type == Token::LOW || type == Token::NORMAL || type == Token::MIDPOINT ||
1,500✔
38
            type == Token::ORIGIN);
750✔
39
}
40

41
bool is_neurite_type(Token id) {
276✔
42
    return (id == Token::AXON || id == Token::APICAL || id == Token::DENDRITE ||
276✔
43
            id == Token::CELLBODY);
276✔
44
}
45

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

54
bool is_end_of_section(Token id) {
1,088✔
55
    return (id == Token::RPAREN || id == Token::PIPE);
1,088✔
56
}
57

58
bool skip_sexp(size_t id) {
1,084✔
59
    return (id == +Token::WORD || id == +Token::COLOR || id == +Token::GENERATED ||
3,152✔
60
            id == +Token::HIGH || id == +Token::INCOMPLETE || id == +Token::LOW ||
3,006✔
61
            id == +Token::NORMAL || id == +Token::FONT);
3,152✔
62
}
63

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

72
    NeurolucidaParser(NeurolucidaParser const&) = delete;
73
    NeurolucidaParser& operator=(NeurolucidaParser const&) = delete;
74

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

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

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

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

102
        lex.consume();
620✔
103

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

109
        lex.consume(Token::RPAREN, "Point should end in RPAREN");
620✔
110

111
        return {{point[0], point[1], point[2]}, point[3]};
620✔
112
    }
113

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

117
        bool ret = true;
98✔
118
        while (true) {
119
            ret &= parse_neurite_section(header);
188✔
120
            if (lex_.ended() ||
468✔
121
                (lex_.current()->id != +Token::PIPE && lex_.current()->id != +Token::LPAREN)) {
282✔
122
                break;
96✔
123
            }
124
            lex_.consume();
90✔
125
        }
126
        lex_.consume(Token::RPAREN, "Branch should end with RPAREN");
96✔
127
        return ret;
96✔
128
    }
129

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

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

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

174
        return return_id;
664✔
175
    }
176

177
    /*
178
      Add the last point of parent section to the beginning of this section
179
      if not already present.
180
      See https://github.com/BlueBrain/MorphIO/pull/221
181

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

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

189
     The idea is that these two structures should represent the same morphology:
190

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

212
        if (lastParentPoint == properties._points[0])
184✔
213
            return;
12✔
214

215
        properties._points.insert(properties._points.begin(), lastParentPoint);
172✔
216
        properties._diameters.insert(properties._diameters.begin(), childSectionNextDiameter);
172✔
217
    }
218

219
    /**
220
       Parse the root sexp until finding the first sexp containing numbers
221
    **/
222
    Header parse_root_sexp_header() {
140✔
223
        Header header;
140✔
224

225
        while (true) {
226
            const Token id = static_cast<Token>(lex_.current()->id);
392✔
227
            const size_t peek_id = lex_.peek()->id;
392✔
228

229
            if (is_eof(id)) {
392✔
230
                throw RawDataError(err_.ERROR_EOF_IN_NEURITE(lex_.line_num()));
×
231
            } else if (id == Token::MARKER) {
392✔
232
                lex_.consume();
×
233
            } else if (id == Token::WORD) {
392✔
234
                lex_.consume_until_balanced_paren();
2✔
235
                lex_.consume(Token::LPAREN);
2✔
236
            } else if (id == Token::STRING) {
390✔
237
                header.label = lex_.current()->str();
48✔
238
                // Get rid of quotes
239
                header.label = header.label.substr(1, header.label.size() - 2);
48✔
240

241
                // Early NeuroLucida files contained the soma in a named String
242
                // s-exp: https://github.com/BlueBrain/MorphIO/issues/300
243
                const std::string uppercase_label = text_to_uppercase_token_string(header.label);
48✔
244
                if (uppercase_label == "CELLBODY") {
48✔
245
                    header.token = Token::CELLBODY;
44✔
246
                }
247

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

276

277
    bool parse_neurite_section(const Header& header) {
336✔
278
        Points points;
672✔
279
        std::vector<morphio::floatType> diameters;
340✔
280
        auto section_id = static_cast<int>(nb_.sections().size());
336✔
281

282
        while (true) {
283
            const auto id = static_cast<Token>(lex_.current()->id);
1,082✔
284
            const size_t peek_id = lex_.peek()->id;
1,082✔
285

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

351
    void parse_root_sexps() {
230✔
352
        // parse the top level blocks, and if they are a neurite, otherwise skip
353
        while (!lex_.ended()) {
230✔
354
            if (static_cast<Token>(lex_.current()->id) == Token::LPAREN) {
180✔
355
                lex_.consume();
140✔
356
                const Header header = parse_root_sexp_header();
280✔
357
                if (lex_.current()->id != +Token::RPAREN) {
140✔
358
                    parse_neurite_section(header);
140✔
359
                }
360
            }
361

362
            if (!lex_.ended())
178✔
363
                lex_.consume();
178✔
364
        }
365
    }
50✔
366

367
    morphio::mut::Morphology nb_;
368

369
    std::string uri_;
370
    NeurolucidaLexer lex_;
371

372
    details::ErrorMessages err_;
373
};
374

375
}  // namespace
376

377
Property::Properties load(const std::string& path,
52✔
378
                          const std::string& contents,
379
                          unsigned int options,
380
                          WarningHandler* warning_handler) {
381
    NeurolucidaParser parser(path);
104✔
382

383
    morphio::mut::Morphology& nb_ = parser.parse(contents);
52✔
384
    nb_.applyModifiers(options);
50✔
385

386
    Property::Properties properties = nb_.buildReadOnly();
50✔
387

388
    switch (properties._somaLevel._points.size()) {
50✔
389
    case 0:
6✔
390
        warning_handler->emit(std::make_shared<NoSomaFound>(path));
6✔
391
        properties._cellLevel._somaType = enums::SOMA_UNDEFINED;
6✔
392
        break;
6✔
393
    case 1:
×
394
        throw RawDataError("Morphology contour with only a single point is not valid: " + path);
×
395
    case 2:
×
396
        properties._cellLevel._somaType = enums::SOMA_UNDEFINED;
×
397
        break;
×
398
    default:
44✔
399
        properties._cellLevel._somaType = enums::SOMA_SIMPLE_CONTOUR;
44✔
400
        break;
44✔
401
    }
402
    properties._cellLevel._cellFamily = NEURON;
50✔
403
    properties._cellLevel._version = {"asc", 1, 0};
50✔
404
    return properties;
100✔
405
}
406

407
}  // namespace asc
408
}  // namespace readers
409
}  // 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