diff --git a/plugins/CircuitExplorer/api/neuron/NeuronMorphologyPipeline.cpp b/plugins/CircuitExplorer/api/neuron/NeuronMorphologyPipeline.cpp index 6d04dc1b9..e79e62b83 100644 --- a/plugins/CircuitExplorer/api/neuron/NeuronMorphologyPipeline.cpp +++ b/plugins/CircuitExplorer/api/neuron/NeuronMorphologyPipeline.cpp @@ -19,6 +19,7 @@ #include "NeuronMorphologyPipeline.h" #include "processors/ConstantRadius.h" +#include "processors/Growth.h" #include "processors/RadiusMultiplier.h" #include "processors/Resampler.h" #include "processors/SectionSmoother.h" @@ -75,6 +76,15 @@ class StageBuilder stages.push_back(std::make_unique(samplingFactor)); } + void addGrowthStage(float growth) + { + if (growth == 1.0f) + { + return; + } + stages.push_back(std::make_unique(growth)); + } + NeuronMorphologyPipeline::Stages stages; }; } @@ -86,6 +96,7 @@ NeuronMorphologyPipeline NeuronMorphologyPipeline::fromParameters(const NeuronMo stageBuilder.addSubsamplingStage(parameters.subsampling); stageBuilder.addResamplingStage(parameters.resampling); stageBuilder.addRadiusStage(parameters.radius_multiplier); + stageBuilder.addGrowthStage(parameters.growth); return NeuronMorphologyPipeline(std::move(stageBuilder.stages)); } diff --git a/plugins/CircuitExplorer/api/neuron/processors/Growth.cpp b/plugins/CircuitExplorer/api/neuron/processors/Growth.cpp new file mode 100644 index 000000000..26a705ca1 --- /dev/null +++ b/plugins/CircuitExplorer/api/neuron/processors/Growth.cpp @@ -0,0 +1,237 @@ +/* Copyright (c) 2015-2024, EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * Responsible Author: Adrien Fleury + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "Growth.h" + +#include +#include +#include +#include + +namespace +{ +using brayns::Vector3f; +using brayns::math::length; + +using Sample = NeuronMorphology::SectionSample; +using Section = NeuronMorphology::Section; +using Hierarchy = std::unordered_map>; + +struct SampleInfo +{ + std::size_t index; + Vector3f position; + float distance = 0.0f; +}; + +struct SectionInfo +{ + std::int32_t id; + std::size_t index; + std::vector samples; + std::vector children; + float length = 0.0f; +}; + +Hierarchy createHierarchy(const std::vector
§ions) +{ + auto hierarchy = Hierarchy(); + + for (auto i = std::size_t(0); i < sections.size(); ++i) + { + hierarchy[sections[i].parentId].push_back(i); + } + + return hierarchy; +} + +std::vector prepareSampleInfos(const std::vector &samples) +{ + auto sampleCount = samples.size(); + + auto infos = std::vector(); + infos.reserve(sampleCount); + + for (auto i = std::size_t(0); i < sampleCount; ++i) + { + infos.push_back({.index = i, .position = samples[i].position}); + } + + return infos; +} + +std::vector prepareSectionInfos( + const std::vector
§ions, + const Hierarchy &hierarchy, + std::int32_t sectionId = -1) +{ + auto i = hierarchy.find(sectionId); + + if (i == hierarchy.end()) + { + return {}; + } + + const auto &childIndices = i->second; + + auto infos = std::vector(); + infos.reserve(childIndices.size()); + + for (auto childIndex : childIndices) + { + const auto &child = sections[childIndex]; + + infos.push_back({ + .id = child.id, + .index = childIndex, + .samples = prepareSampleInfos(child.samples), + .children = prepareSectionInfos(sections, hierarchy, child.id), + }); + } + + return infos; +} + +void computeSampleDistances(std::vector &samples, const SampleInfo *parentSample) +{ + if (samples.empty()) + { + return; + } + + if (parentSample == nullptr) + { + parentSample = &samples[0]; + } + + for (auto &sample : samples) + { + auto vectorToSample = sample.position - parentSample->position; + auto distanceToSample = length(vectorToSample); + + sample.distance = parentSample->distance + distanceToSample; + + parentSample = &sample; + } +} + +void computeSectionDistances(std::vector §ions, const SampleInfo *parentSample = nullptr) +{ + for (auto §ion : sections) + { + computeSampleDistances(section.samples, parentSample); + + if (!section.samples.empty()) + { + parentSample = §ion.samples.back(); + } + + computeSectionDistances(section.children, parentSample); + } +} + +void computeSectionLengths(std::vector §ions, float parentSampleDistance = 0.0f) +{ + for (auto §ion : sections) + { + auto lastSampleDistance = parentSampleDistance; + + if (!section.samples.empty()) + { + lastSampleDistance = section.samples.back().distance; + } + + if (section.children.empty()) + { + section.length = lastSampleDistance; + continue; + } + + computeSectionLengths(section.children, lastSampleDistance); + + auto compareLength = [](const auto &left, const auto &right) { return left.length < right.length; }; + auto longestBranch = std::ranges::max_element(section.children, compareLength); + + section.length = longestBranch->length; + } +} + +std::vector createSectionInfos(const std::vector
§ions) +{ + auto hierarchy = createHierarchy(sections); + + auto infos = prepareSectionInfos(sections, hierarchy); + + computeSectionDistances(infos); + + computeSectionLengths(infos); + + return infos; +} + +bool shouldRemoveSample(const SectionInfo §ion, const SampleInfo &sample, float growth) +{ + assert(section.length != 0.0f); + + auto normalizedDistance = sample.distance / section.length; + + return normalizedDistance >= growth; +} + +void removeUnbornSamples(std::vector
§ions, const std::vector &infos, float growth) +{ + for (const auto &info : infos) + { + auto §ion = sections[info.index]; + auto &samples = section.samples; + + for (const auto &sampleInfo : info.samples) + { + if (shouldRemoveSample(info, sampleInfo, growth)) + { + samples.erase(samples.begin() + sampleInfo.index, samples.end()); + break; + } + } + + removeUnbornSamples(sections, info.children, growth); + } +} + +void removeEmptySections(std::vector
§ions) +{ + auto [first, last] = std::ranges::remove_if(sections, [](auto §ion) { return section.samples.empty(); }); + sections.erase(first, last); +} +} + +Growth::Growth(float percentage): + _percentage(percentage) +{ +} + +void Growth::process(NeuronMorphology &morphology) const +{ + auto §ions = morphology.sections(); + + auto infos = createSectionInfos(sections); + + removeUnbornSamples(sections, infos, _percentage); + + removeEmptySections(sections); +} diff --git a/plugins/CircuitExplorer/api/neuron/processors/Growth.h b/plugins/CircuitExplorer/api/neuron/processors/Growth.h new file mode 100644 index 000000000..8b710a7f7 --- /dev/null +++ b/plugins/CircuitExplorer/api/neuron/processors/Growth.h @@ -0,0 +1,32 @@ +/* Copyright (c) 2015-2024, EPFL/Blue Brain Project + * All rights reserved. Do not distribute without permission. + * Responsible Author: Adrien Fleury + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License version 3.0 as published + * by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include + +class Growth : public INeuronMorphologyProcessor +{ +public: + explicit Growth(float percentage); + + void process(NeuronMorphology &morphology) const override; + +private: + float _percentage; +}; diff --git a/plugins/CircuitExplorer/io/NeuronMorphologyLoaderParameters.h b/plugins/CircuitExplorer/io/NeuronMorphologyLoaderParameters.h index 5d3078d5d..c73fb79df 100644 --- a/plugins/CircuitExplorer/io/NeuronMorphologyLoaderParameters.h +++ b/plugins/CircuitExplorer/io/NeuronMorphologyLoaderParameters.h @@ -33,6 +33,7 @@ struct NeuronMorphologyLoaderParameters NeuronGeometryType geometry_type = NeuronGeometryType::Original; float resampling = 0; uint32_t subsampling = 1; + float growth = 1.0f; }; namespace brayns @@ -113,6 +114,13 @@ struct JsonAdapter : ObjectAdapter Morphology: geometry_type: GeometryType = GeometryType.SMOOTH resampling: float = 2 subsampling: int = 1 + growth: float = 1.0 def serialize_morphology(morphology: Morphology) -> dict[str, Any]: @@ -91,6 +94,7 @@ def serialize_morphology(morphology: Morphology) -> dict[str, Any]: "geometry_type": morphology.geometry_type.value, "resampling": morphology.resampling, "subsampling": morphology.subsampling, + "growth": morphology.growth, } diff --git a/python/tests/plugins/test_bbp.py b/python/tests/plugins/test_bbp.py index 026c3cc88..9540499f1 100644 --- a/python/tests/plugins/test_bbp.py +++ b/python/tests/plugins/test_bbp.py @@ -123,6 +123,7 @@ def test_get_properties(self) -> None: "geometry_type": "original", "resampling": 2.0, "subsampling": 1, + "growth": 1, }, }, ) diff --git a/python/tests/plugins/test_cell_placement.py b/python/tests/plugins/test_cell_placement.py index dc937ce8f..2398a78f7 100644 --- a/python/tests/plugins/test_cell_placement.py +++ b/python/tests/plugins/test_cell_placement.py @@ -55,6 +55,7 @@ def test_get_properties(self) -> None: "radius_multiplier": 1.0, "resampling": 2, "subsampling": 1, + "growth": 1, }, }, ) diff --git a/python/tests/plugins/test_morphology.py b/python/tests/plugins/test_morphology.py index 00a7cb69a..4d68e274e 100644 --- a/python/tests/plugins/test_morphology.py +++ b/python/tests/plugins/test_morphology.py @@ -33,6 +33,7 @@ def test_serialize_morphology(self) -> None: geometry_type=brayns.GeometryType.CONSTANT_RADII, resampling=0.5, subsampling=5, + growth=0.5, ) self.assertEqual( serialize_morphology(test), @@ -44,6 +45,7 @@ def test_serialize_morphology(self) -> None: "geometry_type": brayns.GeometryType.CONSTANT_RADII.value, "resampling": 0.5, "subsampling": 5, + "growth": 0.5, }, ) @@ -72,5 +74,6 @@ def test_properties(self) -> None: "geometry_type": "original", "resampling": 2.0, "subsampling": 1, + "growth": 1, } self.assertEqual(loader.get_properties(), properties) diff --git a/python/tests/plugins/test_sonata.py b/python/tests/plugins/test_sonata.py index b58813091..c1101aa04 100644 --- a/python/tests/plugins/test_sonata.py +++ b/python/tests/plugins/test_sonata.py @@ -172,6 +172,7 @@ def test_properties(self) -> None: "geometry_type": "smooth", "resampling": 2.0, "subsampling": 1, + "growth": 1, }, "vasculature_geometry_parameters": { "radius_multiplier": 2.0, @@ -188,6 +189,7 @@ def test_properties(self) -> None: "geometry_type": "smooth", "resampling": 2, "subsampling": 1, + "growth": 1, }, "vasculature_geometry_parameters": { "radius_multiplier": 1.0,