• 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

94.12
/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 <algorithm>
9
#include <cstdint>        // uint32_t
10
#include <memory>         // std::shared_ptr
11
#include <sstream>        // std::stringstream
12
#include <string>         // std::string
13
#include <unordered_map>  // std::unordered_map
14
#include <vector>         // std::vector
15

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

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

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

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

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

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

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

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

73
}  // unnamed namespace
74

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

88
    void _readSamples(const std::string& contents) {
64✔
89
        std::stringstream stream{contents};
128✔
90
        unsigned int lineNumber = 0;
64✔
91
        std::string line;
128✔
92
        while (!std::getline(stream, line).fail()) {
1,282✔
93
            ++lineNumber;
1,224✔
94

95
            if (line.empty() || _ignoreLine(line)) {
1,224✔
96
                continue;
736✔
97
            }
98

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

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

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

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

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

116
            if (sample.type == SECTION_SOMA) {
482✔
117
                lastSomaPoint = sample.id;
104✔
118
            }
119
        }
120
    }
58✔
121

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

136
    void raiseIfBrokenSoma(const Sample& sample) {
468✔
137
        if (sample.type != SECTION_SOMA) {
468✔
138
            return;
372✔
139
        }
140

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

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

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

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

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

174
    void checkSoma() {
50✔
175
        auto somata = _potentialSomata();
100✔
176

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

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

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

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

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

210
    bool isSectionStart(const Sample& sample) {
454✔
211
        return (isRootPoint(sample) ||
794✔
212
                (sample.parentId != SWC_ROOT &&
340✔
213
                 isSectionEnd(samples[sample.parentId])));  // Standard section
746✔
214
    }
215

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

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

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

239
        pushChildren(pushChildren, SWC_ROOT);
48✔
240

241
        return ret;
96✔
242
    }
243

244
    void raiseIfNonConform(const Sample& sample) {
470✔
245
        raiseIfSelfParent(sample);
470✔
246
        raiseIfBrokenSoma(sample);
468✔
247
        raiseIfNoParent(sample);
464✔
248
        warnIfZeroDiameter(sample);
462✔
249
    }
462✔
250

251
    void _checkNeuroMorphoSoma(const Sample& root, const std::vector<Sample>& _children) {
4✔
252
        floatType x = root.point[0];
4✔
253
        floatType y = root.point[1];
4✔
254
        floatType z = root.point[2];
4✔
255
        floatType d = root.diameter;
4✔
256
        floatType r = root.diameter / 2;
4✔
257
        const Sample& child1 = _children[0];
4✔
258
        const Sample& child2 = _children[1];
4✔
259

260
        // whether the soma should be checked for the special case of 3 point soma
261
        // for details see https://github.com/BlueBrain/MorphIO/issues/273
262
        bool isSuited = std::fabs(child1.diameter - d) < morphio::epsilon &&
8✔
263
                        std::fabs(child2.diameter - d) < morphio::epsilon &&
8✔
264
                        std::fabs(child1.point[0] - x) < morphio::epsilon &&
8✔
265
                        std::fabs(child2.point[0] - x) < morphio::epsilon &&
8✔
266
                        std::fabs(child1.point[2] - z) < morphio::epsilon &&
12✔
267
                        std::fabs(child2.point[2] - z) < morphio::epsilon;
4✔
268
        if (!isSuited) {
4✔
269
            return;
×
270
        }
271
        // If the 2nd and the 3rd point have the same x,z,d values then the only valid soma is:
272
        // 1 1 x   y   z r -1
273
        // 2 1 x (y-r) z r  1
274
        // 3 1 x (y+r) z r  1
275
        if (child1.point[0] != x || child2.point[0] != x || child1.point[1] != y - r ||
8✔
276
            child2.point[1] != y + r || child1.point[2] != z || child2.point[2] != z ||
4✔
277
            child1.diameter != d || child2.diameter != d) {
8✔
278
            printError(Warning::SOMA_NON_CONFORM,
×
279
                       err.WARNING_NEUROMORPHO_SOMA_NON_CONFORM(root, child1, child2));
×
280
        }
281
    }
282

283
    SomaType somaType() {
48✔
284
        switch (morph.soma()->points().size()) {
48✔
285
        case 0: {
×
286
            return SOMA_UNDEFINED;
×
287
        }
288
        case 1: {
34✔
289
            return SOMA_SINGLE_POINT;
34✔
290
        }
291
        case 2: {
2✔
292
            return SOMA_CYLINDERS;
2✔
293
        }
294
        // NeuroMorpho format is characterized by a 3 points soma
295
        // with a bifurcation at soma root
296
        case 3: {
4✔
297
            uint32_t somaRootId = children[SWC_ROOT][0];
4✔
298
            const auto& somaChildren = children[somaRootId];
4✔
299

300
            std::vector<Sample> children_soma_points;
8✔
301
            for (unsigned int child : somaChildren) {
20✔
302
                if (this->samples[child].type == SECTION_SOMA) {
16✔
303
                    children_soma_points.push_back(this->samples[child]);
8✔
304
                }
305
            }
306

307
            if (children_soma_points.size() == 2) {
4✔
308
                //  NeuroMorpho is the main provider of morphologies, but they
309
                //  with SWC as their default file format: they convert all
310
                //  uploads to SWC.  In the process of conversion, they turn all
311
                //  somas into their custom 'Three-point soma representation':
312
                //   http://neuromorpho.org/SomaFormat.html
313

314
                if (!ErrorMessages::isIgnored(Warning::SOMA_NON_CONFORM)) {
4✔
315
                    _checkNeuroMorphoSoma(this->samples[somaRootId], children_soma_points);
4✔
316
                }
317

318
                return SOMA_NEUROMORPHO_THREE_POINT_CYLINDERS;
4✔
319
            }
320
            return SOMA_CYLINDERS;
×
321
        }
322
        default:
8✔
323
            return SOMA_CYLINDERS;
8✔
324
        }
325
    }
326

327
    Property::Properties buildProperties(const std::string& contents, unsigned int options) {
64✔
328
        _readSamples(contents);
64✔
329

330
        for (const auto& sample_pair : samples) {
520✔
331
            const auto& sample = sample_pair.second;
470✔
332
            raiseIfNonConform(sample);
470✔
333
        }
334

335
        checkSoma();
50✔
336

337
        // The process might occasionally creates empty section before
338
        // filling them so the warning is ignored
339
        bool originalIsIgnored = err.isIgnored(morphio::Warning::APPENDING_EMPTY_SECTION);
48✔
340
        set_ignored_warning(morphio::Warning::APPENDING_EMPTY_SECTION, true);
48✔
341

342
        std::vector<unsigned int> depthFirstSamples = constructDepthFirstSamples();
96✔
343
        for (const auto id : depthFirstSamples) {
502✔
344
            const Sample& sample = samples[id];
454✔
345

346
            // Bifurcation right at the start
347
            if (isRootPoint(sample) && isSectionEnd(sample)) {
454✔
348
                continue;
×
349
            }
350

351
            if (isSectionStart(sample)) {
454✔
352
                _processSectionStart(sample);
226✔
353
            } else if (sample.type != SECTION_SOMA) {
228✔
354
                swcIdToSectionId[sample.id] =
292✔
355
                    swcIdToSectionId[static_cast<unsigned int>(sample.parentId)];
146✔
356
            }
357

358
            if (sample.type == SECTION_SOMA) {
454✔
359
                appendSample(morph.soma(), sample);
82✔
360
            } else {
361
                appendSample(morph.section(swcIdToSectionId.at(sample.id)), sample);
372✔
362
            }
363
        }
364

365
        if (morph.soma()->points().size() == 3 && !neurite_wrong_root.empty()) {
48✔
366
            printError(morphio::WRONG_ROOT_POINT, err.WARNING_WRONG_ROOT_POINT(neurite_wrong_root));
×
367
        }
368

369
        morph.applyModifiers(options);
48✔
370

371
        Property::Properties properties = morph.buildReadOnly();
48✔
372
        properties._cellLevel._somaType = somaType();
48✔
373

374
        set_ignored_warning(morphio::Warning::APPENDING_EMPTY_SECTION, originalIsIgnored);
48✔
375

376
        return properties;
96✔
377
    }
378

379
    /**
380
       - Append last point of previous section if current section is not a root
381
    section
382
       - Update the parent ID of the new section
383
    **/
384
    void _processSectionStart(const Sample& sample) {
226✔
385
        Property::PointLevel properties;
226✔
386

387
        uint32_t id = 0;
226✔
388

389
        if (isRootPoint(sample)) {
226✔
390
            id = morph.appendRootSection(properties, sample.type)->id();
114✔
391
        } else {
392
            // Duplicating last point of previous section if there is not already a duplicate
393
            auto parentId = static_cast<unsigned int>(sample.parentId);
112✔
394
            if (sample.point != samples[parentId].point) {
112✔
395
                properties._points.push_back(samples[parentId].point);
104✔
396
                properties._diameters.push_back(samples[parentId].diameter);
104✔
397
            }
398

399
            // Handle the case, bifurcatation at root point
400
            if (isRootPoint(samples[parentId])) {
112✔
401
                id = morph.appendRootSection(properties, sample.type)->id();
×
402
            } else {
403
                id = morph.section(swcIdToSectionId[parentId])
112✔
404
                         ->appendSection(properties, sample.type)
224✔
405
                         ->id();
112✔
406
            }
407
        }
408

409
        swcIdToSectionId[sample.id] = id;
226✔
410
    }
226✔
411

412
  private:
413
    // Dictionary: SWC Id of the last point of a section to morphio::mut::Section ID
414
    std::unordered_map<uint32_t, uint32_t> swcIdToSectionId;
415

416
    // Neurite that do not have parent ID = 1, allowed for soma contour, not
417
    // 3-pts soma
418
    std::vector<Sample> neurite_wrong_root;
419

420
    unsigned int lastSomaPoint = 0;
421
    std::unordered_map<unsigned int, std::vector<unsigned int>> children;
422
    std::unordered_map<unsigned int, Sample> samples;
423
    mut::Morphology morph;
424
    ErrorMessages err;
425
};
426

427
Property::Properties load(const std::string& path,
64✔
428
                          const std::string& contents,
429
                          unsigned int options) {
430
    auto properties = SWCBuilder(path).buildProperties(contents, options);
80✔
431

432
    properties._cellLevel._cellFamily = NEURON;
48✔
433
    properties._cellLevel._version = {"swc", 1, 0};
48✔
434
    return properties;
48✔
435
}
436

437
}  // namespace swc
438
}  // namespace readers
439
}  // 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