Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BRAYNS-637 Morphology growing. #1260

Merged
merged 2 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions plugins/CircuitExplorer/api/neuron/NeuronMorphologyPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -75,6 +76,15 @@ class StageBuilder
stages.push_back(std::make_unique<Subsampler>(samplingFactor));
}

void addGrowthStage(float growth)
{
if (growth == 1.0f)
{
return;
}
stages.push_back(std::make_unique<Growth>(growth));
}

NeuronMorphologyPipeline::Stages stages;
};
}
Expand All @@ -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));
}

Expand Down
237 changes: 237 additions & 0 deletions plugins/CircuitExplorer/api/neuron/processors/Growth.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/* Copyright (c) 2015-2024, EPFL/Blue Brain Project
* All rights reserved. Do not distribute without permission.
* Responsible Author: Adrien Fleury <adrien.fleury@epfl.ch>
*
* 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 <cassert>
#include <ranges>
#include <unordered_map>
#include <vector>

namespace
{
using brayns::Vector3f;
using brayns::math::length;

using Sample = NeuronMorphology::SectionSample;
using Section = NeuronMorphology::Section;
using Hierarchy = std::unordered_map<std::int32_t, std::vector<std::size_t>>;

struct SampleInfo
{
std::size_t index;
Vector3f position;
float distance = 0.0f;
};

struct SectionInfo
{
std::int32_t id;
std::size_t index;
std::vector<SampleInfo> samples;
std::vector<SectionInfo> children;
float length = 0.0f;
};

Hierarchy createHierarchy(const std::vector<Section> &sections)
{
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<SampleInfo> prepareSampleInfos(const std::vector<Sample> &samples)
{
auto sampleCount = samples.size();

auto infos = std::vector<SampleInfo>();
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<SectionInfo> prepareSectionInfos(
const std::vector<Section> &sections,
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<SectionInfo>();
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<SampleInfo> &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<SectionInfo> &sections, const SampleInfo *parentSample = nullptr)
{
for (auto &section : sections)
{
computeSampleDistances(section.samples, parentSample);

if (!section.samples.empty())
{
parentSample = &section.samples.back();
}

computeSectionDistances(section.children, parentSample);
}
}

void computeSectionLengths(std::vector<SectionInfo> &sections, float parentSampleDistance = 0.0f)
{
for (auto &section : 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<SectionInfo> createSectionInfos(const std::vector<Section> &sections)
{
auto hierarchy = createHierarchy(sections);

auto infos = prepareSectionInfos(sections, hierarchy);

computeSectionDistances(infos);

computeSectionLengths(infos);

return infos;
}

bool shouldRemoveSample(const SectionInfo &section, const SampleInfo &sample, float growth)
{
assert(section.length != 0.0f);

auto normalizedDistance = sample.distance / section.length;

return normalizedDistance >= growth;
}

void removeUnbornSamples(std::vector<Section> &sections, const std::vector<SectionInfo> &infos, float growth)
{
for (const auto &info : infos)
{
auto &section = 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<Section> &sections)
{
auto [first, last] = std::ranges::remove_if(sections, [](auto &section) { return section.samples.empty(); });
sections.erase(first, last);
}
}

Growth::Growth(float percentage):
_percentage(percentage)
{
}

void Growth::process(NeuronMorphology &morphology) const
{
auto &sections = morphology.sections();

auto infos = createSectionInfos(sections);

removeUnbornSamples(sections, infos, _percentage);

removeEmptySections(sections);
}
32 changes: 32 additions & 0 deletions plugins/CircuitExplorer/api/neuron/processors/Growth.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* Copyright (c) 2015-2024, EPFL/Blue Brain Project
* All rights reserved. Do not distribute without permission.
* Responsible Author: Adrien Fleury <adrien.fleury@epfl.ch>
*
* 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 <api/neuron/INeuronMorphologyProcessor.h>

class Growth : public INeuronMorphologyProcessor
{
public:
explicit Growth(float percentage);

void process(NeuronMorphology &morphology) const override;

private:
float _percentage;
};
8 changes: 8 additions & 0 deletions plugins/CircuitExplorer/io/NeuronMorphologyLoaderParameters.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct NeuronMorphologyLoaderParameters
NeuronGeometryType geometry_type = NeuronGeometryType::Original;
float resampling = 0;
uint32_t subsampling = 1;
float growth = 1.0f;
};

namespace brayns
Expand Down Expand Up @@ -113,6 +114,13 @@ struct JsonAdapter<NeuronMorphologyLoaderParameters> : ObjectAdapter<NeuronMorph
[](auto &object, auto value) { object.subsampling = value; })
.description("Skip factor when converting samples into geometry (disabled if <= 1)")
.defaultValue(1);
builder
.getset(
"growth",
[](auto &object) { return object.growth; },
[](auto &object, auto value) { object.growth = value; })
.description("Neuron growth [0-1], includes all segments at 1 and none at 0")
.defaultValue(1.0f);
return builder.build();
}
};
Expand Down
4 changes: 4 additions & 0 deletions python/brayns/plugins/morphology.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ class Morphology:
:type resampling: float, optional
:param subsampling: Step factor to skip morphology samples.
:type subsampling: int, optional
:param growth: Morphology growth [0-1], remove all segments at 0, leave unchanged at 1.
:type growth: int, optional
"""

@staticmethod
Expand All @@ -80,6 +82,7 @@ def full() -> Morphology:
geometry_type: GeometryType = GeometryType.SMOOTH
resampling: float = 2
subsampling: int = 1
growth: float = 1.0


def serialize_morphology(morphology: Morphology) -> dict[str, Any]:
Expand All @@ -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,
}


Expand Down
1 change: 1 addition & 0 deletions python/tests/plugins/test_bbp.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def test_get_properties(self) -> None:
"geometry_type": "original",
"resampling": 2.0,
"subsampling": 1,
"growth": 1,
},
},
)
1 change: 1 addition & 0 deletions python/tests/plugins/test_cell_placement.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def test_get_properties(self) -> None:
"radius_multiplier": 1.0,
"resampling": 2,
"subsampling": 1,
"growth": 1,
},
},
)
3 changes: 3 additions & 0 deletions python/tests/plugins/test_morphology.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
},
)

Expand Down Expand Up @@ -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)
Loading