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

BlueBrain / MorphIO / 7111656197

06 Dec 2023 07:50AM UTC coverage: 76.555% (+0.5%) from 76.104%
7111656197

push

github

web-flow
Better read/write for soma types (#411)

* Be strict about soma types: ASC/H5 are contours, SWC is single point, cylinders or neuromorpho 3 point
* when writing, an incorrect soma type will throw ERROR_UNSUPPORTED_SOMA_TYPE
* removed unused ERROR_OPENING_FILE
* changed single point h5/asc files used in tests to be 4 point contours
* For compat, we will for now print a warning if the soma_type isn't set

103 of 138 new or added lines in 6 files covered. (74.64%)

6 existing lines in 3 files now uncovered.

1982 of 2589 relevant lines covered (76.55%)

882.46 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

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

11

12
#include "NeurolucidaLexer.inc"
13
#include "morphio/enums.h"
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
    Header()
148✔
25
        : token(static_cast<Token>(+Token::STRING))
148✔
26
        , parent_id(-1) {}
148✔
27
    Token token;
28
    std::string label;
29
    int32_t parent_id;
30
};
31

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

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

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

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

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

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

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

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

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

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

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

104
        lex.consume();
620✔
105

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

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

113
        return {{point[0], point[1], point[2]}, point[3]};
620✔
114
    }
115

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

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

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

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

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

176
        return return_id;
664✔
177
    }
178

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

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

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

191
     The idea is that these two structures should represent the same morphology:
192

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

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

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

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

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

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

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

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

278

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

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

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

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

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

369
    morphio::mut::Morphology nb_;
370

371
    std::string uri_;
372
    NeurolucidaLexer lex_;
373

374
    ErrorMessages err_;
375
};
376

377
}  // namespace
378

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

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

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

389
    switch (properties._somaLevel._points.size()) {
50✔
390
    case 0:
6✔
391
        properties._cellLevel._somaType = enums::SOMA_UNDEFINED;
6✔
392
        break;
6✔
NEW
393
    case 1:
×
NEW
394
        throw RawDataError("Morphology contour with only a single point is not valid: " + path);
×
NEW
395
    case 2:
×
NEW
396
        properties._cellLevel._somaType = enums::SOMA_UNDEFINED;
×
NEW
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