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

BlueBrain / MorphIO / 7286001939

21 Dec 2023 08:57AM UTC coverage: 76.104% (-0.8%) from 76.938%
7286001939

Pull #485

github

mgeplf
try scikit-build-core
Pull Request #485: try scikit-build-core

1930 of 2536 relevant lines covered (76.1%)

863.39 hits per line

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

89.22
/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

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

11

12
#include "NeurolucidaLexer.inc"
13

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

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

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

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

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

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

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

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

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

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

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

81
  private:
82
    std::tuple<Point, floatType> parse_point(NeurolucidaLexer& lex, bool is_marker) {
476✔
83
        lex.expect(Token::LPAREN, "Point should start in LPAREN");
476✔
84
        std::array<morphio::floatType, 4> point{};  // X,Y,Z,D
476✔
85
        for (unsigned int i = 0; i < 4; i++) {
2,380✔
86
            try {
87
#ifdef MORPHIO_USE_DOUBLE
88
                point[i] = std::stod(lex.consume()->str());
89
#else
90
                point[i] = std::stof(lex.consume()->str());
1,904✔
91
#endif
92
            } catch (const std::invalid_argument&) {
×
93
                throw RawDataError(err_.ERROR_PARSING_POINT(lex.line_num(), lex.current()->str()));
×
94
            }
95

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

103
        lex.consume();
476✔
104

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

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

112
        return {{point[0], point[1], point[2]}, point[3]};
476✔
113
    }
114

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

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

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

139
        if (header.token == Token::STRING) {
318✔
140
            Property::Marker marker;
12✔
141
            marker._pointLevel = properties;
12✔
142
            marker._label = header.label;
12✔
143
            marker._sectionId = header.parent_id;
12✔
144
            nb_.addMarker(marker);
12✔
145
            return_id = -1;
12✔
146
        } else if (header.token == Token::CELLBODY) {
306✔
147
            if (!nb_.soma()->points().empty())
42✔
148
                throw SomaError(err_.ERROR_SOMA_ALREADY_DEFINED(lex_.line_num()));
×
149
            nb_.soma()->properties() = properties;
42✔
150
            return_id = -1;
42✔
151
        } else {
152
            SectionType section_type = TokenToSectionType(header.token);
264✔
153
            insertLastPointParentSection(header.parent_id, properties, diameters);
264✔
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) {
264✔
159
                return_id = header.parent_id;
×
160
            } else {
161
                std::shared_ptr<morphio::mut::Section> section;
×
162
                if (header.parent_id > -1)
264✔
163
                    section = nb_.section(static_cast<unsigned int>(header.parent_id))
176✔
164
                                  ->appendSection(properties, section_type);
176✔
165
                else
166
                    section = nb_.appendRootSection(properties, section_type);
88✔
167
                return_id = static_cast<int>(section->id());
264✔
168
            }
169
        }
170
        points.clear();
318✔
171
        diameters.clear();
318✔
172

173
        return return_id;
636✔
174
    }
175

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

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

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

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

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

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

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

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

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

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

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

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

275

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

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

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

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

361
            if (!lex_.ended())
170✔
362
                lex_.consume();
170✔
363
        }
364
    }
48✔
365

366
    morphio::mut::Morphology nb_;
367

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

371
    ErrorMessages err_;
372
};
373

374
}  // namespace
375

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

381
    morphio::mut::Morphology& nb_ = parser.parse(contents);
50✔
382
    nb_.applyModifiers(options);
48✔
383

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

390
}  // namespace asc
391
}  // namespace readers
392
}  // 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