• 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

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

6
#include "morphologySWC.h"
7

8
#include <cstdint>        // uint32_t
9
#include <memory>         // std::shared_ptr
10
#include <sstream>        // std::stringstream
11
#include <string>         // std::string
12
#include <unordered_map>  // std::unordered_map
13
#include <vector>         // std::vector
14

15
#include <morphio/errorMessages.h>
16
#include <morphio/mut/morphology.h>
17
#include <morphio/mut/section.h>
18
#include <morphio/mut/soma.h>
19
#include <morphio/properties.h>
20

21
#include "../shared_utils.hpp"
22

23
namespace {
24
// It's not clear if -1 is the only way of identifying the root section.
25
const int SWC_UNDEFINED_PARENT = -1;
26
const unsigned int SWC_ROOT = 0xFFFFFFFD;
27

28
bool _ignoreLine(const std::string& line) {
1,420✔
29
    std::size_t pos = line.find_first_not_of("\n\r\t ");
1,420✔
30
    return pos == std::string::npos || line[pos] == '#';
1,420✔
31
}
32

33
morphio::readers::Sample readSample(const char* line, unsigned int lineNumber_) {
626✔
34
#ifdef MORPHIO_USE_DOUBLE
35
    const char* const format = "%d%d%lg%lg%lg%lg%d";
36
#else
37
    const char* const format = "%d%d%f%f%f%f%d";
626✔
38
#endif
39

40
    morphio::floatType radius = -1.;
626✔
41
    int int_type = -1;
626✔
42
    morphio::readers::Sample sample;
626✔
43
    int parentId = -1;
626✔
44
    int id = -1;
626✔
45
    sample.valid = sscanf(line,
626✔
46
                          format,
47
                          &id,
48
                          &int_type,
49
                          &sample.point[0],
626✔
50
                          &sample.point[1],
626✔
51
                          &sample.point[2],
626✔
52
                          &radius,
53
                          &parentId) == 7;
626✔
54

55
    if (id < 0) {
626✔
56
        throw morphio::RawDataError("Negative IDs are not supported");
×
57
    }
58
    sample.id = static_cast<unsigned int>(id);
626✔
59

60
    if (parentId < -1) {
626✔
61
        throw morphio::RawDataError("Negative ParentIDs are not supported");
×
62
    } else if (parentId == SWC_UNDEFINED_PARENT) {
626✔
63
        sample.parentId = SWC_ROOT;
76✔
64
    } else {
65
        sample.parentId = static_cast<unsigned int>(parentId);
550✔
66
    }
67

68
    sample.type = static_cast<morphio::SectionType>(int_type);
626✔
69
    sample.diameter = radius * 2;  // The point array stores diameters.
626✔
70
    sample.lineNumber = lineNumber_;
626✔
71
    return sample;
1,252✔
72
}
73

74
}  // unnamed namespace
75

76
namespace morphio {
77
namespace readers {
78
namespace swc {
79
/**
80
   Parsing SWC according to this specification:
81
   http://www.neuronland.org/NLMorphologyConverter/MorphologyFormats/SWC/Spec.html
82
**/
83
class SWCBuilder
84
{
85
  public:
86
    explicit SWCBuilder(const std::string& path)
74✔
87
        : err(path) {}
74✔
88

89
    void _readSamples(const std::string& contents) {
74✔
90
        std::stringstream stream{contents};
148✔
91
        unsigned int lineNumber = 0;
74✔
92
        std::string line;
148✔
93
        while (!std::getline(stream, line).fail()) {
1,602✔
94
            ++lineNumber;
1,534✔
95

96
            if (line.empty() || _ignoreLine(line)) {
1,534✔
97
                continue;
908✔
98
            }
99

100
            const auto& sample = readSample(line.data(), lineNumber);
626✔
101

102
            if (!sample.valid) {
626✔
103
                throw RawDataError(err.ERROR_LINE_NON_PARSABLE(lineNumber));
2✔
104
            }
105

106
            if (sample.type >= SECTION_OUT_OF_RANGE_START || sample.type <= 0) {
624✔
107
                throw RawDataError(err.ERROR_UNSUPPORTED_SECTION_TYPE(lineNumber, sample.type));
2✔
108
            }
109

110
            if (samples.count(sample.id) > 0) {
622✔
111
                throw RawDataError(err.ERROR_REPEATED_ID(samples[sample.id], sample));
2✔
112
            }
113

114
            samples[sample.id] = sample;
620✔
115
            children[sample.parentId].push_back(sample.id);
620✔
116

117
            if (sample.type == SECTION_SOMA) {
620✔
118
                lastSomaPoint = sample.id;
122✔
119
            }
120
        }
121
    }
68✔
122

123
    /**
124
       Are considered potential somata all sample
125
       with parentId == SWC_ROOT and sample.type == SECTION_SOMA
126
     **/
127
    std::vector<Sample> _potentialSomata() {
60✔
128
        std::vector<Sample> somata;
60✔
129
        for (auto id : children[SWC_ROOT]) {
122✔
130
            if (samples[id].type == SECTION_SOMA) {
62✔
131
                somata.push_back(samples[id]);
62✔
132
            }
133
        }
134
        return somata;
60✔
135
    }
136

137
    void raiseIfBrokenSoma(const Sample& sample) {
606✔
138
        if (sample.type != SECTION_SOMA) {
606✔
139
            return;
492✔
140
        }
141

142
        if (sample.parentId != SWC_ROOT && !children[sample.id].empty()) {
114✔
143
            std::vector<Sample> soma_bifurcations;
36✔
144
            for (unsigned int id : children[sample.id]) {
46✔
145
                if (samples[id].type == SECTION_SOMA) {
28✔
146
                    soma_bifurcations.push_back(samples[id]);
20✔
147
                } else {
148
                    neurite_wrong_root.push_back(samples[id]);
8✔
149
                }
150
            }
151

152
            if (soma_bifurcations.size() > 1) {
18✔
153
                throw morphio::SomaError(err.ERROR_SOMA_BIFURCATION(sample, soma_bifurcations));
2✔
154
            }
155
        }
156

157
        if (sample.parentId != SWC_ROOT && samples.count(sample.parentId) > 0 &&
160✔
158
            samples[sample.parentId].type != SECTION_SOMA) {
48✔
159
            throw morphio::SomaError(err.ERROR_SOMA_WITH_NEURITE_PARENT(sample));
2✔
160
        }
161
    }
162

163
    void raiseIfSelfParent(const Sample& sample) {
608✔
164
        if (sample.parentId == sample.id) {
608✔
165
            throw morphio::RawDataError(err.ERROR_SELF_PARENT(sample));
2✔
166
        }
167
    }
606✔
168

169
    void warnIfDisconnectedNeurite(const Sample& sample) {
592✔
170
        if (sample.parentId == SWC_ROOT && sample.type != SECTION_SOMA) {
592✔
171
            printError(Warning::DISCONNECTED_NEURITE, err.WARNING_DISCONNECTED_NEURITE(sample));
×
172
        }
173
    }
592✔
174

175
    void checkSoma() {
60✔
176
        auto somata = _potentialSomata();
120✔
177

178
        if (somata.size() > 1) {
60✔
179
            throw morphio::SomaError(err.ERROR_MULTIPLE_SOMATA(somata));
2✔
180
        }
181

182
        if (somata.empty()) {
58✔
183
            printError(Warning::NO_SOMA_FOUND, err.WARNING_NO_SOMA_FOUND());
×
184
        } else {
185
            for (const auto& sample_pair : samples) {
650✔
186
                const auto& sample = sample_pair.second;
592✔
187
                warnIfDisconnectedNeurite(sample);
592✔
188
            }
189
        }
190
    }
58✔
191

192
    void raiseIfNoParent(const Sample& sample) {
602✔
193
        if (sample.parentId != SWC_ROOT && samples.count(sample.parentId) == 0) {
602✔
194
            throw morphio::MissingParentError(err.ERROR_MISSING_PARENT(sample));
2✔
195
        }
196
    }
600✔
197

198
    void warnIfZeroDiameter(const Sample& sample) {
600✔
199
        if (sample.diameter < morphio::epsilon) {
600✔
UNCOV
200
            printError(Warning::ZERO_DIAMETER, err.WARNING_ZERO_DIAMETER(sample));
×
201
        }
202
    }
600✔
203

204
    bool isRootPoint(const Sample& sample) {
1,598✔
205
        bool isOrphanNeurite = sample.parentId == SWC_ROOT && sample.type != SECTION_SOMA;
1,598✔
206
        return isOrphanNeurite ||
3,196✔
207
               (sample.type != SECTION_SOMA &&
1,598✔
208
                samples[sample.parentId].type == SECTION_SOMA);  // Exclude soma bifurcations
2,996✔
209
    }
210

211
    bool isSectionStart(const Sample& sample) {
592✔
212
        return (isRootPoint(sample) ||
1,042✔
213
                (sample.parentId != SWC_ROOT &&
450✔
214
                 isSectionEnd(samples[sample.parentId])));  // Standard section
984✔
215
    }
216

217
    bool isSectionEnd(const Sample& sample) {
534✔
218
        return sample.id == lastSomaPoint ||        // End of soma
534✔
219
               children[sample.id].empty() ||       // Reached leaf
1,228✔
220
               (children[sample.id].size() >= 2 &&  // Reached neurite bifurcation
534✔
221
                sample.type != SECTION_SOMA);
694✔
222
    }
223

224
    template <typename T>
225
    void appendSample(const std::shared_ptr<T>& somaOrSection, const Sample& sample) {
592✔
226
        somaOrSection->points().push_back(sample.point);
592✔
227
        somaOrSection->diameters().push_back(sample.diameter);
592✔
228
    }
592✔
229

230
    std::vector<unsigned int> constructDepthFirstSamples() {
58✔
231
        std::vector<unsigned int> ret;
58✔
232
        ret.reserve(samples.size());
58✔
233
        const auto pushChildren = [&](const auto& f, unsigned int id) -> void {
650✔
234
            for (unsigned int childId : children[id]) {
1,242✔
235
                ret.push_back(childId);
592✔
236
                f(f, childId);
592✔
237
            }
238
        };
650✔
239

240
        pushChildren(pushChildren, SWC_ROOT);
58✔
241

242
        return ret;
116✔
243
    }
244

245
    void raiseIfNonConform(const Sample& sample) {
608✔
246
        raiseIfSelfParent(sample);
608✔
247
        raiseIfBrokenSoma(sample);
606✔
248
        raiseIfNoParent(sample);
602✔
249
        warnIfZeroDiameter(sample);
600✔
250
    }
600✔
251

252
    SomaType somaType() {
58✔
253
        switch (morph.soma()->points().size()) {
58✔
254
        case 0: {
×
255
            return SOMA_UNDEFINED;
×
256
        }
257
        case 1: {
40✔
258
            return SOMA_SINGLE_POINT;
40✔
259
        }
260
        case 2: {
2✔
261
            return SOMA_CYLINDERS;
2✔
262
        }
263
        // NeuroMorpho format is characterized by a 3 points soma
264
        // with a bifurcation at soma root
265
        case 3: {
8✔
266
            uint32_t somaRootId = children[SWC_ROOT][0];
8✔
267
            const auto& somaChildren = children[somaRootId];
8✔
268

269
            std::vector<Sample> children_soma_points;
16✔
270
            for (unsigned int child : somaChildren) {
48✔
271
                if (this->samples[child].type == SECTION_SOMA) {
40✔
272
                    children_soma_points.push_back(this->samples[child]);
16✔
273
                }
274
            }
275

276
            if (children_soma_points.size() == 2) {
8✔
277
                if (!ErrorMessages::isIgnored(Warning::SOMA_NON_CONFORM)) {
8✔
278
                    const std::array<Point, 3> points = {
279
                        samples[somaRootId].point,
8✔
280
                        children_soma_points[0].point,
8✔
281
                        children_soma_points[1].point,
8✔
282
                    };
8✔
283
                    details::ThreePointSomaStatus status =
284
                        details::checkNeuroMorphoSoma(points, samples[somaRootId].diameter / 2);
8✔
285

286
                    if (status != details::ThreePointSomaStatus::Conforms) {
8✔
NEW
287
                        std::stringstream stream;
×
NEW
288
                        stream << status;
×
NEW
289
                        printError(Warning::SOMA_NON_CONFORM,
×
NEW
290
                                   err.WARNING_NEUROMORPHO_SOMA_NON_CONFORM(stream.str()));
×
291
                    }
292
                }
293

294
                return SOMA_NEUROMORPHO_THREE_POINT_CYLINDERS;
8✔
295
            }
296
            return SOMA_CYLINDERS;
×
297
        }
298
        default:
8✔
299
            return SOMA_CYLINDERS;
8✔
300
        }
301
    }
302

303
    Property::Properties buildProperties(const std::string& contents, unsigned int options) {
74✔
304
        _readSamples(contents);
74✔
305

306
        for (const auto& sample_pair : samples) {
668✔
307
            const auto& sample = sample_pair.second;
608✔
308
            raiseIfNonConform(sample);
608✔
309
        }
310

311
        checkSoma();
60✔
312

313
        // The process might occasionally creates empty section before
314
        // filling them so the warning is ignored
315
        bool originalIsIgnored = err.isIgnored(morphio::Warning::APPENDING_EMPTY_SECTION);
58✔
316
        set_ignored_warning(morphio::Warning::APPENDING_EMPTY_SECTION, true);
58✔
317

318
        std::vector<unsigned int> depthFirstSamples = constructDepthFirstSamples();
116✔
319
        for (const auto id : depthFirstSamples) {
650✔
320
            const Sample& sample = samples[id];
592✔
321

322
            // Bifurcation right at the start
323
            if (isRootPoint(sample) && isSectionEnd(sample)) {
592✔
324
                continue;
×
325
            }
326

327
            if (isSectionStart(sample)) {
592✔
328
                _processSectionStart(sample);
278✔
329
            } else if (sample.type != SECTION_SOMA) {
314✔
330
                swcIdToSectionId[sample.id] =
428✔
331
                    swcIdToSectionId[static_cast<unsigned int>(sample.parentId)];
214✔
332
            }
333

334
            if (sample.type == SECTION_SOMA) {
592✔
335
                appendSample(morph.soma(), sample);
100✔
336
            } else {
337
                appendSample(morph.section(swcIdToSectionId.at(sample.id)), sample);
492✔
338
            }
339
        }
340

341
        if (morph.soma()->points().size() == 3 && !neurite_wrong_root.empty()) {
58✔
342
            printError(morphio::WRONG_ROOT_POINT, err.WARNING_WRONG_ROOT_POINT(neurite_wrong_root));
×
343
        }
344

345
        morph.applyModifiers(options);
58✔
346

347
        Property::Properties properties = morph.buildReadOnly();
58✔
348
        properties._cellLevel._somaType = somaType();
58✔
349

350
        set_ignored_warning(morphio::Warning::APPENDING_EMPTY_SECTION, originalIsIgnored);
58✔
351

352
        return properties;
116✔
353
    }
354

355
    /*
356
    Append last point of previous section if current section is not a root section
357
    Update the parent ID of the new section
358
    */
359
    void _processSectionStart(const Sample& sample) {
278✔
360
        Property::PointLevel properties;
278✔
361

362
        uint32_t id = 0;
278✔
363

364
        if (isRootPoint(sample)) {
278✔
365
            id = morph.appendRootSection(properties, sample.type)->id();
142✔
366
        } else {
367
            // Duplicating last point of previous section if there is not already a duplicate
368
            auto parentId = static_cast<unsigned int>(sample.parentId);
136✔
369
            if (sample.point != samples[parentId].point) {
136✔
370
                properties._points.push_back(samples[parentId].point);
136✔
371
                properties._diameters.push_back(samples[parentId].diameter);
136✔
372
            }
373

374
            // Handle the case, bifurcatation at root point
375
            if (isRootPoint(samples[parentId])) {
136✔
376
                id = morph.appendRootSection(properties, sample.type)->id();
×
377
            } else {
378
                id = morph.section(swcIdToSectionId[parentId])
136✔
379
                         ->appendSection(properties, sample.type)
272✔
380
                         ->id();
136✔
381
            }
382
        }
383

384
        swcIdToSectionId[sample.id] = id;
278✔
385
    }
278✔
386

387
  private:
388
    // Dictionary: SWC Id of the last point of a section to morphio::mut::Section ID
389
    std::unordered_map<uint32_t, uint32_t> swcIdToSectionId;
390

391
    // Neurite that do not have parent ID = 1, allowed for soma contour, not
392
    // 3-pts soma
393
    std::vector<Sample> neurite_wrong_root;
394

395
    unsigned int lastSomaPoint = 0;
396
    std::unordered_map<unsigned int, std::vector<unsigned int>> children;
397
    std::unordered_map<unsigned int, Sample> samples;
398
    mut::Morphology morph;
399
    ErrorMessages err;
400
};
401

402
Property::Properties load(const std::string& path,
74✔
403
                          const std::string& contents,
404
                          unsigned int options) {
405
    auto properties = SWCBuilder(path).buildProperties(contents, options);
90✔
406

407
    properties._cellLevel._cellFamily = NEURON;
58✔
408
    properties._cellLevel._version = {"swc", 1, 0};
58✔
409
    return properties;
58✔
410
}
411

412
}  // namespace swc
413
}  // namespace readers
414
}  // 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