Repository: BlueBrain/MorphIO (language: python)

Issue:
Improve ascii morphology reading performance
After running some quick benchmarks between the previous reading code (which reads char by char using istreambuf_iterator) and this method, which reads the whole file in blocks (maybe in a single read?), We have obtained the following reading measures:

Old method average time (milliseconds): 1299.375
New method average time (milliseconds): 6.71

The benchmark code is this:
```
#include <morphio/morphology.h>
#include <morphio/section.h>
#include <morphio/soma.h>

#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>

int main(int, char**) {
    const auto path = std::filesystem::path(/* path to BBPTestData ascii morphology folder */);

    const auto files = std::filesystem::directory_iterator(path);

    auto start = std::chrono::high_resolution_clock::now();

    for (const auto& file : files) {
        const auto filePath = file.path().string();

        /*
        std::ifstream ifs(filePath, std::ios::ate);
        const auto size = ifs.tellg();
        ifs.seekg(0);
        std::string input;
        input.resize(static_cast<size_t>(size));
        ifs.read(&input[0], size);
        ifs.close();
        */

        std::ifstream ifs(filePath);
        std::string input((std::istreambuf_iterator<char>(ifs)),
                          (std::istreambuf_iterator<char>()));

        }
    auto end = std::chrono::high_resolution_clock::now();
    auto diff = end - start;
    auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(diff);
    std::cout << "Time: " << millis.count() << " ms" << std::endl;
    return 0;
}
```


Discussion / hints:
Cool!  Let me investigate the `pip install --use-feature=in-tree-build` error separately.
Just opened a new PR about that. They updated pip yesterday to 22.1, which removes that option and makes it default behaviour
Thank you, I'm very interested in this. I'll build a TouchDetector using it, and will see how our morphology loading does. Also compare to HDF5 loading.
I dont fully get why the unittests are failing on Windows
> I dont fully get why the unittests are failing on Windows

I'll have to look into it; adding it to the pile
Apparently windows does not like this way of reading files?
Reading a 2MB ASC, morphology, I get these numbers:
```
old;
BM_load_morphology/asc             26.9 ms

new:
BM_load_morphology/asc             20.9 ms
```
Using:
```
std::ifstream ifs(uri_);
std::ostringstream oss;
oss << ifs.rdbuf();
std::string input{oss.str()};
```
Looks safer to me, but isn't quite as fast: 
```
BM_load_morphology/asc             22.3 ms         22.3 ms           31
```

As @eleftherioszisis; points out `tellg` has some fun behavior: https://stackoverflow.com/a/22986486
Thats a pity. I tried a 9 MB file, its 30x faster to read.
Would it be possible to modify MorphIO Morphology API, adding a constructor that can accept the data?
Brayns is a UNIX-only software, so we are safe to use tellg(), and we could greatly improve the performance on our side.
> Would it be possible to modify MorphIO Morphology API, adding a constructor that can accept the data?

Seconded, that would allow to store morphologies in a format better for GPFS :)

HDF5 already does that by allowing to pass a group from an open file rather than a path to a file.
> Thats a pity. I tried a 9 MB file, its 30x faster to read.

Is that with the  `oss << ifs.rdbuf()` method?  It seems to be cross platform, and nearly as fast.

> Would it be possible to modify MorphIO Morphology API, adding a constructor that can accept the data?

Yeah, that's certainly something that could be done.

Fix patch:
diff --git a/binds/python/bind_immutable.cpp b/binds/python/bind_immutable.cpp
index 7a7b65703..7c44ab9ee 100644
--- a/binds/python/bind_immutable.cpp
+++ b/binds/python/bind_immutable.cpp
@@ -34,6 +34,10 @@ void bind_immutable_module(py::module& m) {
         .def(py::init<const std::string&, unsigned int>(),
              "filename"_a,
              "options"_a = morphio::enums::Option::NO_MODIFIER)
+        .def(py::init<const std::string&, const std::string&, unsigned int>(),
+             "filename"_a,
+             "extension"_a,
+             "options"_a = morphio::enums::Option::NO_MODIFIER)
         .def(py::init<morphio::mut::Morphology&>())
         .def(py::init([](py::object arg, unsigned int options) {
                  return std::make_unique<morphio::Morphology>(py::str(arg), options);
diff --git a/include/morphio/errorMessages.h b/include/morphio/errorMessages.h
index 696c09dcf..d20bc12e8 100644
--- a/include/morphio/errorMessages.h
+++ b/include/morphio/errorMessages.h
@@ -68,32 +68,9 @@ static std::set<Warning> _ignoredWarnings;
 struct Sample {
     Sample() = default;
 
-    explicit Sample(const char* line, unsigned int lineNumber_)
-        : lineNumber(lineNumber_) {
-        floatType radius = -1.;
-        int int_type = -1;
-#ifdef MORPHIO_USE_DOUBLE
-        const auto format = "%20u%20d%20lg%20lg%20lg%20lg%20d";
-#else
-        const auto format = "%20u%20d%20f%20f%20f%20f%20d";
-#endif
-        valid = sscanf(line,
-                       format,
-                       &id,
-                       &int_type,
-                       &point[0],
-                       &point[1],
-                       &point[2],
-                       &radius,
-                       &parentId) == 7;
-
-        type = static_cast<SectionType>(int_type);
-        diameter = radius * 2;  // The point array stores diameters.
-    }
-
     floatType diameter = -1.;
     bool valid = false;
-    Point point;
+    Point point{};
     SectionType type = SECTION_UNDEFINED;
     int parentId = -1;
     unsigned int id = 0;
diff --git a/include/morphio/morphology.h b/include/morphio/morphology.h
index b1b2e9fbe..9596bd108 100644
--- a/include/morphio/morphology.h
+++ b/include/morphio/morphology.h
@@ -38,13 +38,19 @@ class Morphology
         Example:
             Morphology("neuron.asc", TWO_POINTS_SECTIONS | SOMA_SPHERE);
      */
-    explicit Morphology(const std::string& source, unsigned int options = NO_MODIFIER);
+    explicit Morphology(const std::string& path, unsigned int options = NO_MODIFIER);
 
     /** Constructor from an already parsed file */
     explicit Morphology(const HighFive::Group& group, unsigned int options = NO_MODIFIER);
+
     /** Constructor from an instance of morphio::mut::Morphology */
     explicit Morphology(const mut::Morphology&);
 
+    /** Load a morphology from a string */
+    explicit Morphology(const std::string& contents,
+                        const std::string& extension,
+                        unsigned int options = NO_MODIFIER);
+
     /** Return the soma object */
     Soma soma() const;
 
diff --git a/src/morphology.cpp b/src/morphology.cpp
index e8b6a2af4..f7109c77d 100644
--- a/src/morphology.cpp
+++ b/src/morphology.cpp
@@ -1,4 +1,6 @@
+#include <cctype>  // std::tolower
 #include <fstream>
+#include <iterator>  // std::back_inserter
 #include <memory>
 
 #include <morphio/endoplasmic_reticulum.h>
@@ -15,7 +17,20 @@
 
 namespace {
 
-void buildChildren(const std::shared_ptr<morphio::Property::Properties> properties) {
+std::string readCompleteFile(const std::string& path) {
+    std::ifstream ifs(path);
+
+    if (!ifs) {
+        throw(morphio::RawDataError("File: " + path + " does not exist."));
+    }
+
+    std::ostringstream oss;
+    oss << ifs.rdbuf();
+
+    return oss.str();
+}
+
+void buildChildren(const std::shared_ptr<morphio::Property::Properties>& properties) {
     {
         const auto& sections = properties->get<morphio::Property::Section>();
         auto& children = properties->_sectionLevel._children;
@@ -51,32 +66,52 @@ morphio::SomaType getSomaType(long unsigned int num_soma_points) {
     return morphio::SOMA_SIMPLE_CONTOUR;
 }
 
-morphio::Property::Properties loadURI(const std::string& source, unsigned int options) {
-    const size_t pos = source.find_last_of('.');
-    if (pos == std::string::npos) {
+std::string tolower(const std::string& str) {
+    std::string ret;
+    std::transform(str.begin(), str.end(), std::back_inserter(ret), [](unsigned char c) {
+        return std::tolower(c);
+    });
+    return ret;
+}
+
+morphio::Property::Properties loadFile(const std::string& path, unsigned int options) {
+    const size_t pos = path.find_last_of('.');
+    if (pos == std::string::npos || pos == path.length() - 1) {
         throw(morphio::UnknownFileType("File has no extension"));
     }
 
-    // Cross-platform check of file existance
-    std::ifstream file(source.c_str());
-    if (!file) {
-        throw(morphio::RawDataError("File: " + source + " does not exist."));
+    std::string extension = tolower(path.substr(pos + 1));
+
+    if (extension == "h5") {
+        return morphio::readers::h5::load(path);
+    } else if (extension == "asc") {
+        std::string contents = readCompleteFile(path);
+        return morphio::readers::asc::load(path, contents, options);
+    } else if (extension == "swc") {
+        std::string contents = readCompleteFile(path);
+        return morphio::readers::swc::load(path, contents, options);
     }
 
-    std::string extension = source.substr(pos);
+    throw(morphio::UnknownFileType("Unhandled file type: '" + extension +
+                                   "' only SWC, ASC and H5 are supported"));
+}
+
+
+morphio::Property::Properties loadString(const std::string& contents,
+                                         const std::string& extension,
+                                         unsigned int options) {
+    std::string lower_extension = tolower(extension);
 
-    if (extension == ".h5" || extension == ".H5") {
-        return morphio::readers::h5::load(source);
-    } else if (extension == ".asc" || extension == ".ASC") {
-        return morphio::readers::asc::load(source, options);
-    } else if (extension == ".swc" || extension == ".SWC") {
-        return morphio::readers::swc::load(source, options);
+    if (lower_extension == "asc") {
+        return morphio::readers::asc::load("$STRING$", contents, options);
+    } else if (lower_extension == "swc") {
+        return morphio::readers::swc::load("$STRING$", contents, options);
     }
 
-    throw(morphio::UnknownFileType("Unhandled file type: only SWC, ASC and H5 are supported"));
+    throw(morphio::UnknownFileType("Unhandled file type: '" + lower_extension +
+                                   "' only SWC, ASC and H5 are supported"));
 }
 
-
 }  // namespace
 
 namespace morphio {
@@ -99,17 +134,22 @@ Morphology::Morphology(const Property::Properties& properties, unsigned int opti
     }
 }
 
+Morphology::Morphology(const std::string& path, unsigned int options)
+    : Morphology(loadFile(path, options), options) {}
+
 Morphology::Morphology(const HighFive::Group& group, unsigned int options)
     : Morphology(readers::h5::load(group), options) {}
 
-Morphology::Morphology(const std::string& source, unsigned int options)
-    : Morphology(loadURI(source, options), options) {}
-
 Morphology::Morphology(const mut::Morphology& morphology) {
     properties_ = std::make_shared<Property::Properties>(morphology.buildReadOnly());
     buildChildren(properties_);
 }
 
+Morphology::Morphology(const std::string& contents,
+                       const std::string& extension,
+                       unsigned int options)
+    : Morphology(loadString(contents, extension, options), options) {}
+
 Soma Morphology::soma() const {
     return Soma(properties_);
 }
diff --git a/src/mut/morphology.cpp b/src/mut/morphology.cpp
index 6cce2b286..c5c640452 100644
--- a/src/mut/morphology.cpp
+++ b/src/mut/morphology.cpp
@@ -1,5 +1,6 @@
 #include <cassert>
 #include <cctype>  // std::tolower
+#include <iterator>  // std::back_inserter
 #include <sstream>
 #include <string>
 
diff --git a/src/readers/morphologyASC.cpp b/src/readers/morphologyASC.cpp
index 653b2cf8c..8a0b57dc4 100644
--- a/src/readers/morphologyASC.cpp
+++ b/src/readers/morphologyASC.cpp
@@ -1,7 +1,5 @@
 #include "morphologyASC.h"
 
-#include <fstream>
-
 #include <morphio/mut/morphology.h>
 #include <morphio/mut/section.h>
 
@@ -69,15 +67,9 @@ class NeurolucidaParser
     NeurolucidaParser(NeurolucidaParser const&) = delete;
     NeurolucidaParser& operator=(NeurolucidaParser const&) = delete;
 
-    morphio::mut::Morphology& parse() {
-        std::ifstream ifs(uri_);
-        std::string input((std::istreambuf_iterator<char>(ifs)),
-                          (std::istreambuf_iterator<char>()));
-
+    morphio::mut::Morphology& parse(const std::string& input) {
         lex_.start_parse(input);
-
         parse_root_sexps();
-
         return nb_;
     }
 
@@ -376,10 +368,12 @@ class NeurolucidaParser
 
 }  // namespace
 
-Property::Properties load(const std::string& uri, unsigned int options) {
-    NeurolucidaParser parser(uri);
+Property::Properties load(const std::string& path,
+                          const std::string& contents,
+                          unsigned int options) {
+    NeurolucidaParser parser(path);
 
-    morphio::mut::Morphology& nb_ = parser.parse();
+    morphio::mut::Morphology& nb_ = parser.parse(contents);
     nb_.applyModifiers(options);
 
     Property::Properties properties = nb_.buildReadOnly();
diff --git a/src/readers/morphologyASC.h b/src/readers/morphologyASC.h
index 5ae25f817..bde2eab1f 100644
--- a/src/readers/morphologyASC.h
+++ b/src/readers/morphologyASC.h
@@ -4,7 +4,9 @@
 namespace morphio {
 namespace readers {
 namespace asc {
-Property::Properties load(const std::string& path, unsigned int options);
+Property::Properties load(const std::string& path,
+                          const std::string& contents,
+                          unsigned int options);
 }  // namespace asc
 }  // namespace readers
 }  // namespace morphio
diff --git a/src/readers/morphologySWC.cpp b/src/readers/morphologySWC.cpp
index f78c3ac99..5db84a7ee 100644
--- a/src/readers/morphologySWC.cpp
+++ b/src/readers/morphologySWC.cpp
@@ -1,11 +1,11 @@
 #include "morphologySWC.h"
 
-#include <cstdint>  // uint32_t
-#include <fstream>
-#include <map>     // std::map
-#include <memory>  // std::shared_ptr
-#include <string>  // std::string
-#include <vector>  // std::vector
+#include <cstdint>        // uint32_t
+#include <memory>         // std::shared_ptr
+#include <sstream>        // std::stringstream
+#include <string>         // std::string
+#include <unordered_map>  // std::unordered_map
+#include <vector>         // std::vector
 
 #include <morphio/errorMessages.h>
 #include <morphio/mut/morphology.h>
@@ -19,6 +19,32 @@ bool _ignoreLine(const std::string& line) {
     return pos == std::string::npos || line[pos] == '#';
 }
 
+morphio::readers::Sample readSample(const char* line, unsigned int lineNumber_) {
+#ifdef MORPHIO_USE_DOUBLE
+    const char *const format = "%20u%20d%20lg%20lg%20lg%20lg%20d";
+#else
+    const char *const format = "%20u%20d%20f%20f%20f%20f%20d";
+#endif
+
+    morphio::floatType radius = -1.;
+    int int_type = -1;
+    morphio::readers::Sample sample;
+    sample.valid = sscanf(line,
+                          format,
+                          &sample.id,
+                          &int_type,
+                          &sample.point[0],
+                          &sample.point[1],
+                          &sample.point[2],
+                          &radius,
+                          &sample.parentId) == 7;
+
+    sample.type = static_cast<morphio::SectionType>(int_type);
+    sample.diameter = radius * 2;  // The point array stores diameters.
+    sample.lineNumber = lineNumber_;
+    return sample;
+}
+
 }  // unnamed namespace
 
 namespace morphio {
@@ -34,43 +60,34 @@ const int SWC_UNDEFINED_PARENT = -1;
 class SWCBuilder
 {
   public:
-    explicit SWCBuilder(const std::string& _uri)
-        : uri(_uri)
-        , err(_uri)
-        , debugInfo(_uri) {
-        _readSamples();
-
-        for (const auto& sample_pair : samples) {
-            const auto& sample = sample_pair.second;
-            raiseIfNonConform(sample);
-        }
-
-        checkSoma();
+    explicit SWCBuilder(const std::string& path)
+        : err(path) {
     }
 
-    void _readSamples() {
-        std::ifstream file(uri.c_str());
-        if (file.fail())
-            throw morphio::RawDataError(err.ERROR_OPENING_FILE());
-
+    void _readSamples(const std::string& contents) {
+        std::stringstream stream{contents};
         unsigned int lineNumber = 0;
         std::string line;
-        while (!std::getline(file, line).fail()) {
+        while (!std::getline(stream, line).fail()) {
             ++lineNumber;
 
-            if (line.empty() || _ignoreLine(line))
+            if (line.empty() || _ignoreLine(line)) {
                 continue;
+            }
 
-            const auto& sample = Sample(line.data(), lineNumber);
-            if (!sample.valid)
-                throw morphio::RawDataError(err.ERROR_LINE_NON_PARSABLE(lineNumber));
+            const auto& sample = readSample(line.data(), lineNumber);
 
-            if (sample.type >= SECTION_OUT_OF_RANGE_START || sample.type <= 0)
-                throw morphio::RawDataError(
-                    err.ERROR_UNSUPPORTED_SECTION_TYPE(lineNumber, sample.type));
+            if (!sample.valid) {
+                throw RawDataError(err.ERROR_LINE_NON_PARSABLE(lineNumber));
+            }
 
-            if (samples.count(sample.id) > 0)
-                throw morphio::RawDataError(err.ERROR_REPEATED_ID(samples[sample.id], sample));
+            if (sample.type >= SECTION_OUT_OF_RANGE_START || sample.type <= 0) {
+                throw RawDataError(err.ERROR_UNSUPPORTED_SECTION_TYPE(lineNumber, sample.type));
+            }
+
+            if (samples.count(sample.id) > 0) {
+                throw RawDataError(err.ERROR_REPEATED_ID(samples[sample.id], sample));
+            }
 
             samples[sample.id] = sample;
             children[sample.parentId].push_back(sample.id);
@@ -88,49 +105,57 @@ class SWCBuilder
     std::vector<Sample> _potentialSomata() {
         std::vector<Sample> somata;
         for (auto id : children[-1]) {
-            if (samples[id].type == SECTION_SOMA)
+            if (samples[id].type == SECTION_SOMA) {
                 somata.push_back(samples[id]);
+            }
         }
         return somata;
     }
 
     void raiseIfBrokenSoma(const Sample& sample) {
-        if (sample.type != SECTION_SOMA)
+        if (sample.type != SECTION_SOMA) {
             return;
+        }
 
         if (sample.parentId != -1 && !children[static_cast<int>(sample.id)].empty()) {
             std::vector<Sample> soma_bifurcations;
             for (auto id : children[static_cast<int>(sample.id)]) {
-                if (samples[id].type == SECTION_SOMA)
+                if (samples[id].type == SECTION_SOMA) {
                     soma_bifurcations.push_back(samples[id]);
-                else
+                } else {
                     neurite_wrong_root.push_back(samples[id]);
+                }
             }
 
-            if (soma_bifurcations.size() > 1)
+            if (soma_bifurcations.size() > 1) {
                 throw morphio::SomaError(err.ERROR_SOMA_BIFURCATION(sample, soma_bifurcations));
+            }
         }
 
         if (sample.parentId != -1 &&
-            samples[static_cast<unsigned int>(sample.parentId)].type != SECTION_SOMA)
+            samples[static_cast<unsigned int>(sample.parentId)].type != SECTION_SOMA) {
             throw morphio::SomaError(err.ERROR_SOMA_WITH_NEURITE_PARENT(sample));
+        }
     }
 
     void raiseIfSelfParent(const Sample& sample) {
-        if (sample.parentId == static_cast<int>(sample.id))
+        if (sample.parentId == static_cast<int>(sample.id)) {
             throw morphio::RawDataError(err.ERROR_SELF_PARENT(sample));
+        }
     }
 
     void warnIfDisconnectedNeurite(const Sample& sample) {
-        if (sample.parentId == SWC_UNDEFINED_PARENT && sample.type != SECTION_SOMA)
+        if (sample.parentId == SWC_UNDEFINED_PARENT && sample.type != SECTION_SOMA) {
             printError(Warning::DISCONNECTED_NEURITE, err.WARNING_DISCONNECTED_NEURITE(sample));
+        }
     }
 
     void checkSoma() {
         auto somata = _potentialSomata();
 
-        if (somata.size() > 1)
+        if (somata.size() > 1) {
             throw morphio::SomaError(err.ERROR_MULTIPLE_SOMATA(somata));
+        }
 
         if (somata.empty()) {
             printError(Warning::NO_SOMA_FOUND, err.WARNING_NO_SOMA_FOUND());
@@ -143,37 +168,40 @@ class SWCBuilder
     }
 
     void raiseIfNoParent(const Sample& sample) {
-        if (sample.parentId > -1 && samples.count(static_cast<unsigned int>(sample.parentId)) == 0)
+        if (sample.parentId > -1 &&
+            samples.count(static_cast<unsigned int>(sample.parentId)) == 0) {
             throw morphio::MissingParentError(err.ERROR_MISSING_PARENT(sample));
+        }
     }
 
     void warnIfZeroDiameter(const Sample& sample) {
-        if (sample.diameter < morphio::epsilon)
+        if (sample.diameter < morphio::epsilon) {
             printError(Warning::ZERO_DIAMETER, err.WARNING_ZERO_DIAMETER(sample));
+        }
     }
 
     /**
        A neurite which is not attached to the soma
     **/
-    inline bool isOrphanNeurite(const Sample& sample) {
+    bool isOrphanNeurite(const Sample& sample) {
         return (sample.parentId == SWC_UNDEFINED_PARENT && sample.type != SECTION_SOMA);
     }
 
-    inline bool isRootPoint(const Sample& sample) {
+    bool isRootPoint(const Sample& sample) {
         return isOrphanNeurite(sample) ||
                (sample.type != SECTION_SOMA &&
                 samples[static_cast<unsigned int>(sample.parentId)].type ==
                     SECTION_SOMA);  // Exclude soma bifurcations
     }
 
-    inline bool isSectionStart(const Sample& sample) {
+    bool isSectionStart(const Sample& sample) {
         return (isRootPoint(sample) ||
                 (sample.parentId > -1 &&
                  isSectionEnd(samples[static_cast<unsigned int>(sample.parentId)])));  // Standard
                                                                                        // section
     }
 
-    inline bool isSectionEnd(const Sample& sample) {
+    bool isSectionEnd(const Sample& sample) {
         int id = static_cast<int>(sample.id);
         return id == lastSomaPoint ||        // End of soma
                children[id].empty() ||       // Reached leaf
@@ -184,7 +212,6 @@ class SWCBuilder
 
     template <typename T>
     void appendSample(const std::shared_ptr<T>& somaOrSection, const Sample& sample) {
-        debugInfo.setLineNumber(sample.id, sample.lineNumber);
         somaOrSection->points().push_back(sample.point);
         somaOrSection->diameters().push_back(sample.diameter);
     }
@@ -220,8 +247,9 @@ class SWCBuilder
                         std::fabs(child2.point[0] - x) < morphio::epsilon &&
                         std::fabs(child1.point[2] - z) < morphio::epsilon &&
                         std::fabs(child2.point[2] - z) < morphio::epsilon;
-        if (!isSuited)
+        if (!isSuited) {
             return;
+        }
         // If the 2nd and the 3rd point have the same x,z,d values then the only valid soma is:
         // 1 1 x   y   z r -1
         // 2 1 x (y-r) z r  1
@@ -253,8 +281,9 @@ class SWCBuilder
 
             std::vector<Sample> children_soma_points;
             for (auto child : somaChildren) {
-                if (this->samples[child].type == SECTION_SOMA)
+                if (this->samples[child].type == SECTION_SOMA) {
                     children_soma_points.push_back(this->samples[child]);
+                }
             }
 
             if (children_soma_points.size() == 2) {
@@ -264,8 +293,9 @@ class SWCBuilder
                 //  somas into their custom 'Three-point soma representation':
                 //   http://neuromorpho.org/SomaFormat.html
 
-                if (!ErrorMessages::isIgnored(Warning::SOMA_NON_CONFORM))
+                if (!ErrorMessages::isIgnored(Warning::SOMA_NON_CONFORM)) {
                     _checkNeuroMorphoSoma(this->samples[somaRootId], children_soma_points);
+                }
 
                 return SOMA_NEUROMORPHO_THREE_POINT_CYLINDERS;
             }
@@ -276,7 +306,16 @@ class SWCBuilder
         }
     }
 
-    Property::Properties _buildProperties(unsigned int options) {
+    Property::Properties buildProperties(const std::string& contents, unsigned int options) {
+        _readSamples(contents);
+
+        for (const auto& sample_pair : samples) {
+            const auto& sample = sample_pair.second;
+            raiseIfNonConform(sample);
+        }
+
+        checkSoma();
+
         // The process might occasionally creates empty section before
         // filling them so the warning is ignored
         bool originalIsIgnored = err.isIgnored(morphio::Warning::APPENDING_EMPTY_SECTION);
@@ -306,8 +345,9 @@ class SWCBuilder
             }
         }
 
-        if (morph.soma()->points().size() == 3 && !neurite_wrong_root.empty())
+        if (morph.soma()->points().size() == 3 && !neurite_wrong_root.empty()) {
             printError(morphio::WRONG_ROOT_POINT, err.WARNING_WRONG_ROOT_POINT(neurite_wrong_root));
+        }
 
         morph.applyModifiers(options);
 
@@ -354,23 +394,24 @@ class SWCBuilder
 
   private:
     // Dictionary: SWC Id of the last point of a section to morphio::mut::Section ID
-    std::map<uint32_t, uint32_t> swcIdToSectionId;
+    std::unordered_map<uint32_t, uint32_t> swcIdToSectionId;
 
     // Neurite that do not have parent ID = 1, allowed for soma contour, not
     // 3-pts soma
     std::vector<Sample> neurite_wrong_root;
 
     int lastSomaPoint = -1;
-    std::map<int32_t, std::vector<uint32_t>> children;
-    std::map<uint32_t, Sample> samples;
+    std::unordered_map<int32_t, std::vector<uint32_t>> children;
+    std::unordered_map<uint32_t, Sample> samples;
     mut::Morphology morph;
-    std::string uri;
     ErrorMessages err;
-    DebugInfo debugInfo;
 };
 
-Property::Properties load(const std::string& uri, unsigned int options) {
-    auto properties = SWCBuilder(uri)._buildProperties(options);
+Property::Properties load(const std::string& path,
+                          const std::string& contents,
+                          unsigned int options) {
+    auto properties = SWCBuilder(path).buildProperties(contents, options);
+
     properties._cellLevel._cellFamily = NEURON;
     properties._cellLevel._version = {"swc", 1, 0};
     return properties;
diff --git a/src/readers/morphologySWC.h b/src/readers/morphologySWC.h
index 80da2662c..08e6e87fd 100644
--- a/src/readers/morphologySWC.h
+++ b/src/readers/morphologySWC.h
@@ -6,9 +6,9 @@
 namespace morphio {
 namespace readers {
 namespace swc {
-Property::Properties load(const std::string& uri, unsigned int options);
+Property::Properties load(const std::string& path,
+                          const std::string& contents,
+                          unsigned int options);
 }  // namespace swc
-
 }  // namespace readers
-
 }  // namespace morphio


Test patch:
diff --git a/tests/test_1_swc.py b/tests/test_1_swc.py
index b6ddc05bb..c4671a140 100644
--- a/tests/test_1_swc.py
+++ b/tests/test_1_swc.py
@@ -12,6 +12,26 @@
 _path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
 
 
+def test_build_from_string():
+    contents =('''1 1 0 4 0 3.0 -1
+                  2 3 0 0 2 0.5 1
+                  3 3 0 0 3 0.5 2
+                  4 3 0 0 4 0.5 3
+                  5 3 0 0 5 0.5 4''')
+    n = Morphology(contents, "swc")
+    assert_array_equal(n.soma.points, [[0, 4, 0]])
+    assert_array_equal(n.soma.diameters, [6.0])
+    assert len(n.root_sections) == 1
+    assert n.root_sections[0].id == 0
+    assert len(n.root_sections) == 1
+    assert_array_equal(n.root_sections[0].points,
+                       np.array([[0, 0, 2],
+                                 [0, 0, 3],
+                                 [0, 0, 4],
+                                 [0, 0, 5]]))
+    assert_array_equal(n.section_offsets, [0, 4])
+
+
 def test_read_single_neurite(tmp_path):
     '''Test reading a simple neuron consisting of a point soma
     and a single branch neurite.'''
diff --git a/tests/test_2_neurolucida.py b/tests/test_2_neurolucida.py
index daa1a3d03..0d1593a31 100644
--- a/tests/test_2_neurolucida.py
+++ b/tests/test_2_neurolucida.py
@@ -673,3 +673,11 @@ def test_marker_with_string():
                                                      dtype=np.float32))
 def test_version():
     assert_array_equal(Morphology(DATA_DIR / 'simple.asc').version, ('asc', 1, 0))
+
+def test_load_from_string(tmp_path):
+    SIMPLE = Morphology(DATA_DIR / 'simple.asc')
+    with open(DATA_DIR / 'simple.asc', 'r') as fd:
+        contents = fd.read()
+    from_string = Morphology(contents, "asc")
+
+    assert_array_equal(from_string.points, SIMPLE.points)