From 97c36dd3a407b5a424838326a84e0f4b8be3b7b7 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Mon, 26 May 2025 11:40:12 +0200 Subject: [PATCH 01/44] make install -e work --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 457b232..401474f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,3 +9,7 @@ build-backend = "setuptools.build_meta" [tool.pytest.ini_options] testpaths = ["python/tests"] + +[tool.setuptools_scm] +version_scheme = "guess-next-dev" +local_scheme = "no-local-version" From fad6ae64e3f305943b5c39a3b8accc9577dae43d Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Mon, 26 May 2025 13:12:14 +0200 Subject: [PATCH 02/44] wip --- python/tests/test_nodesets.py | 2 +- python/tests/test_population.py | 2 +- tests/data/{ => config}/node_sets.json | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename tests/data/{ => config}/node_sets.json (100%) diff --git a/python/tests/test_nodesets.py b/python/tests/test_nodesets.py index 1bc539d..d64b4df 100644 --- a/python/tests/test_nodesets.py +++ b/python/tests/test_nodesets.py @@ -161,7 +161,7 @@ def test_NodeSet_toJSON(self): ns1 = NodeSets(new) self.assertEqual(new, ns1.toJSON()) - ns = NodeSets.from_file(os.path.join(PATH, 'node_sets.json')) + ns = NodeSets.from_file(os.path.join(PATH, 'config/node_sets.json')) self.assertEqual(new, ns.toJSON()) def test_NodeSetEmptyArray(self): diff --git a/python/tests/test_population.py b/python/tests/test_population.py index 45ac046..9738e95 100644 --- a/python/tests/test_population.py +++ b/python/tests/test_population.py @@ -293,6 +293,6 @@ def test_path_ctor(self): SpikeReader(path / 'spikes.h5') SomaReportReader(path / 'somas.h5') ElementReportReader(path / 'elements.h5') - NodeSets.from_file(path / 'node_sets.json') + NodeSets.from_file(path / 'config/node_sets.json') CircuitConfig.from_file(path / 'config/circuit_config.json') SimulationConfig.from_file(path / 'config/simulation_config.json') diff --git a/tests/data/node_sets.json b/tests/data/config/node_sets.json similarity index 100% rename from tests/data/node_sets.json rename to tests/data/config/node_sets.json From d1f5220aa71d3b593cbde20ab4f3ec6a5b4fca7a Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Mon, 26 May 2025 13:30:31 +0200 Subject: [PATCH 03/44] nodesets in correct folder --- tests/test_node_sets.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_node_sets.cpp b/tests/test_node_sets.cpp index e427775..226bad9 100644 --- a/tests/test_node_sets.cpp +++ b/tests/test_node_sets.cpp @@ -299,7 +299,7 @@ TEST_CASE("NodeSet") { NodeSets ns1(j); CHECK(ns0.toJSON() == ns1.toJSON()); - auto ns = NodeSets::fromFile("./data/node_sets.json"); + auto ns = NodeSets::fromFile("./data/config/node_sets.json"); CHECK(ns.toJSON() == ns1.toJSON()); } From b0b38717baeab4e23b86fcab876dc02436cc7ceb Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Mon, 26 May 2025 13:33:09 +0200 Subject: [PATCH 04/44] new license? --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index a662ed4..f5c4903 100644 --- a/setup.py +++ b/setup.py @@ -112,11 +112,8 @@ def build_extension(self, ext): author="Blue Brain Project, EPFL", long_description=README, long_description_content_type='text/x-rst', - license="LGPLv3", + license="LGPL-3.0-or-later", url='https://github.com/openbraininstitute/libsonata', - classifiers=[ - "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", - ], ext_modules=[CMakeExtension("libsonata._libsonata")], cmdclass={'build_ext': CMakeBuild, }, From 48ea649553697f85df69bd9e78bc4a0438a06558 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Mon, 26 May 2025 15:15:34 +0200 Subject: [PATCH 05/44] add compartment_sets_file to SimulationConfig --- include/bbp/sonata/config.h | 12 ++++++++++++ python/bindings.cpp | 3 +++ python/generated/docstrings.h | 5 +++++ python/tests/test_config.py | 9 +++++---- src/config.cpp | 14 ++++++++++++++ tests/data/config/compartment_sets.json | 0 tests/data/config/simulation_config.json | 4 +++- 7 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 tests/data/config/compartment_sets.json diff --git a/include/bbp/sonata/config.h b/include/bbp/sonata/config.h index bf671e2..bf7394f 100644 --- a/include/bbp/sonata/config.h +++ b/include/bbp/sonata/config.h @@ -170,6 +170,11 @@ class SONATA_API CircuitConfig */ const std::string& getNodeSetsPath() const; + /** + * Returns the path to the compartment sets file. + */ + const std::string& getCompartmentSetsPath() const; + /** * Returns a set with all available population names across all the node networks. */ @@ -781,6 +786,11 @@ class SONATA_API SimulationConfig */ const std::string& getNodeSetsFile() const noexcept; + /** + * Returns the path of compartment sets file + */ + const std::string& getCompartmentSetsFile() const noexcept; + /** * Returns the name of node set to be instantiated for the simulation, default = None */ @@ -827,6 +837,8 @@ class SONATA_API SimulationConfig SimulatorType _targetSimulator; // Path of node sets file std::string _nodeSetsFile; + // Path of compartment sets file + std::string _compartmentSetsFile; // Name of node set nonstd::optional _nodeSet{nonstd::nullopt}; // Remarks on the simulation diff --git a/python/bindings.cpp b/python/bindings.cpp index 14a13e3..084d084 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -1139,6 +1139,9 @@ PYBIND11_MODULE(_libsonata, m) { .def_property_readonly("node_sets_file", &SimulationConfig::getNodeSetsFile, DOC_SIMULATIONCONFIG(getNodeSetsFile)) + .def_property_readonly("compartment_sets_file", + &SimulationConfig::getCompartmentSetsFile, + DOC_SIMULATIONCONFIG(getCompartmentSetsFile)) .def_property_readonly("node_set", &SimulationConfig::getNodeSet, DOC_SIMULATIONCONFIG(getNodeSet)) diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 37a40ab..a7d5f3f 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -1261,6 +1261,9 @@ static const char *__doc_bbp_sonata_SimulationConfig_getNodeSetsFile = R"doc(Returns the path of node sets file overriding node_sets_file provided in _network, default is empty in case of no setting in _network)doc"; +static const char *__doc_bbp_sonata_SimulationConfig_getCompartmentSetsFile = +R"doc(Returns the path of compartment sets file, default is empty in case of no setting in _network)doc"; + static const char *__doc_bbp_sonata_SimulationConfig_getOutput = R"doc(Returns the Output section of the simulation configuration.)doc"; static const char *__doc_bbp_sonata_SimulationConfig_getReport = @@ -1292,6 +1295,8 @@ static const char *__doc_bbp_sonata_SimulationConfig_nodeSet = R"doc()doc"; static const char *__doc_bbp_sonata_SimulationConfig_nodeSetsFile = R"doc()doc"; +static const char *__doc_bbp_sonata_SimulationConfig_compartmentSetsFile = R"doc()doc"; + static const char *__doc_bbp_sonata_SimulationConfig_output = R"doc()doc"; static const char *__doc_bbp_sonata_SimulationConfig_reports = R"doc()doc"; diff --git a/python/tests/test_config.py b/python/tests/test_config.py index 94b8388..5391620 100644 --- a/python/tests/test_config.py +++ b/python/tests/test_config.py @@ -471,10 +471,11 @@ def test_basic(self): self.assertEqual(self.config.network, os.path.abspath(os.path.join(PATH, 'config/circuit_config.json'))) - self.assertEqual(self.config.target_simulator.name, 'CORENEURON'); - circuit_conf = CircuitConfig.from_file(self.config.network); - self.assertEqual(self.config.node_sets_file, circuit_conf.node_sets_path); - self.assertEqual(self.config.node_set, 'Column'); + self.assertEqual(self.config.target_simulator.name, 'CORENEURON') + circuit_conf = CircuitConfig.from_file(self.config.network) + self.assertEqual(self.config.node_sets_file, circuit_conf.node_sets_path) + self.assertEqual(self.config.compartment_sets_file, os.path.abspath(os.path.join(PATH, 'config/compartment_sets.json'))) + self.assertEqual(self.config.node_set, 'Column') self.assertEqual(self.config.list_input_names, {"ex_abs_shotnoise", diff --git a/src/config.cpp b/src/config.cpp index 2974d60..2272e2a 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -1207,6 +1207,15 @@ class SimulationConfig::Parser } } + std::string parseCompartmentSetsFile() const noexcept { + std::string val; + if (_json.contains("compartment_sets_file")) { + val = _json["compartment_sets_file"]; + return toAbsolute(_basePath, val); + } + return val; + } + nonstd::optional parseNodeSet() const { if (_json.contains("node_set")) { return {_json["node_set"]}; @@ -1380,6 +1389,7 @@ SimulationConfig::SimulationConfig(const std::string& content, const std::string _connection_overrides = parser.parseConnectionOverrides(); _targetSimulator = parser.parseTargetSimulator(); _nodeSetsFile = parser.parseNodeSetsFile(); + _compartmentSetsFile = parser.parseCompartmentSetsFile(); _nodeSet = parser.parseNodeSet(); _metaData = parser.parseMetaData(); _betaFeatures = parser.parseBetaFeatures(); @@ -1454,6 +1464,10 @@ const std::string& SimulationConfig::getNodeSetsFile() const noexcept { return _nodeSetsFile; } +const std::string& SimulationConfig::getCompartmentSetsFile() const noexcept { + return _compartmentSetsFile; +} + const nonstd::optional& SimulationConfig::getNodeSet() const noexcept { return _nodeSet; } diff --git a/tests/data/config/compartment_sets.json b/tests/data/config/compartment_sets.json new file mode 100644 index 0000000..e69de29 diff --git a/tests/data/config/simulation_config.json b/tests/data/config/simulation_config.json index 42e579c..594750a 100644 --- a/tests/data/config/simulation_config.json +++ b/tests/data/config/simulation_config.json @@ -13,10 +13,12 @@ }, "manifest": { "$OUTPUT_DIR": "./some/path", - "$ELECTRODES_DIR": "./electrodes/" + "$ELECTRODES_DIR": "./electrodes/", + "$BASE_DIR": "./" }, "target_simulator": "CORENEURON", "node_set" : "Column", + "compartment_sets_file": "$BASE_DIR/compartment_sets.json", "run": { "tstop": 1000, "dt": 0.025, From 543dc902afb436fc14918d19b9ef4ab1f12e5768 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Mon, 26 May 2025 16:20:52 +0200 Subject: [PATCH 06/44] WIP --- CMakeLists.txt | 1 + include/bbp/sonata/compartment_sets.h | 43 +++++++++++++ python/bindings.cpp | 6 ++ python/libsonata/__init__.py | 2 + python/tests/test_compartmentsets.py | 12 ++++ python/tests/test_config.py | 2 +- python/tests/test_nodesets.py | 2 +- python/tests/test_population.py | 2 +- src/compartment_sets.cpp | 64 +++++++++++++++++++ tests/data/{config => }/compartment_sets.json | 0 tests/data/{config => }/node_sets.json | 0 tests/test_node_sets.cpp | 2 +- 12 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 include/bbp/sonata/compartment_sets.h create mode 100644 python/tests/test_compartmentsets.py create mode 100644 src/compartment_sets.cpp rename tests/data/{config => }/compartment_sets.json (100%) rename tests/data/{config => }/node_sets.json (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f9fb40..ed5033e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ set(SONATA_SRC src/hdf5_mutex.cpp src/hdf5_reader.cpp src/node_sets.cpp + src/compartment_sets.cpp src/nodes.cpp src/population.cpp src/report_reader.cpp diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h new file mode 100644 index 0000000..dd4158a --- /dev/null +++ b/include/bbp/sonata/compartment_sets.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +namespace bbp { +namespace sonata { +namespace detail { +class CompartmentSets; +} // namespace detail + +class SONATA_API CompartmentSets +{ + public: + /** + * Create compartmentset from JSON + * + * See also: + * TODO + * + * \param content is the JSON node_sets value + * \throw if content cannot be parsed + */ + explicit CompartmentSets(const std::string& content); + explicit CompartmentSets(std::unique_ptr&& impl); + CompartmentSets(CompartmentSets&&) noexcept; + CompartmentSets(const CompartmentSets& other) = delete; + CompartmentSets& operator=(CompartmentSets&&) noexcept; + ~CompartmentSets(); + + /** Open a SONATA `Compartment sets` file from a path */ + static CompartmentSets fromFile(const std::string& path); + + /** + * Return the nodesets as a JSON string. + */ + std::string toJSON() const; + + private: + std::unique_ptr impl_; +}; + +} // namespace sonata +} // namespace bbp diff --git a/python/bindings.cpp b/python/bindings.cpp index 084d084..15a2495 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include //nonstd::optional #include @@ -535,6 +536,11 @@ PYBIND11_MODULE(_libsonata, m) { .def("update", &NodeSets::update, "other"_a, DOC_NODESETS(update)) .def("toJSON", &NodeSets::toJSON, DOC_NODESETS(toJSON)); + py::class_(m, "CompartmentSets", "CompartmentSets") + .def(py::init()) + .def_static("from_file", [](py::object path) { return CompartmentSets::fromFile(py::str(path)); }) + .def("toJSON", &CompartmentSets::toJSON, DOC_COMPARTMENTSETS(toJSON)); + py::class_(m, "CommonPopulationProperties", "Stores population-specific network information") diff --git a/python/libsonata/__init__.py b/python/libsonata/__init__.py index 4416155..61ea915 100644 --- a/python/libsonata/__init__.py +++ b/python/libsonata/__init__.py @@ -14,6 +14,7 @@ ElementReportReader, NodePopulation, NodeSets, + CompartmentSets, NodeStorage, Selection, SomaDataFrame, @@ -38,6 +39,7 @@ "ElementReportReader", "NodePopulation", "NodeSets", + "CompartmentSets", "NodeStorage", "Selection", "SomaDataFrame", diff --git a/python/tests/test_compartmentsets.py b/python/tests/test_compartmentsets.py new file mode 100644 index 0000000..39d0626 --- /dev/null +++ b/python/tests/test_compartmentsets.py @@ -0,0 +1,12 @@ +import json +import os +import unittest + +from libsonata import ( + CompartmentSets, + SonataError, +) + +class TestCompartmentSetsFailure(unittest.TestCase): + def test_CorrectStructure(self): + self.assertRaises(SonataError, CompartmentSets, "1") \ No newline at end of file diff --git a/python/tests/test_config.py b/python/tests/test_config.py index 5391620..aec7700 100644 --- a/python/tests/test_config.py +++ b/python/tests/test_config.py @@ -16,7 +16,7 @@ def setUp(self): def test_basic(self): self.assertEqual(self.config.node_sets_path, - os.path.abspath(os.path.join(PATH, 'config/node_sets.json'))) + os.path.abspath(os.path.join(PATH, 'node_sets.json'))) self.assertEqual(self.config.node_populations, {'nodes-A', 'nodes-B'}) diff --git a/python/tests/test_nodesets.py b/python/tests/test_nodesets.py index d64b4df..1bc539d 100644 --- a/python/tests/test_nodesets.py +++ b/python/tests/test_nodesets.py @@ -161,7 +161,7 @@ def test_NodeSet_toJSON(self): ns1 = NodeSets(new) self.assertEqual(new, ns1.toJSON()) - ns = NodeSets.from_file(os.path.join(PATH, 'config/node_sets.json')) + ns = NodeSets.from_file(os.path.join(PATH, 'node_sets.json')) self.assertEqual(new, ns.toJSON()) def test_NodeSetEmptyArray(self): diff --git a/python/tests/test_population.py b/python/tests/test_population.py index 9738e95..45ac046 100644 --- a/python/tests/test_population.py +++ b/python/tests/test_population.py @@ -293,6 +293,6 @@ def test_path_ctor(self): SpikeReader(path / 'spikes.h5') SomaReportReader(path / 'somas.h5') ElementReportReader(path / 'elements.h5') - NodeSets.from_file(path / 'config/node_sets.json') + NodeSets.from_file(path / 'node_sets.json') CircuitConfig.from_file(path / 'config/circuit_config.json') SimulationConfig.from_file(path / 'config/simulation_config.json') diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp new file mode 100644 index 0000000..9174521 --- /dev/null +++ b/src/compartment_sets.cpp @@ -0,0 +1,64 @@ +#include // std::find, std::transform +#include +#include +#include +#include +#include + +#include "../extlib/filesystem.hpp" + +#include +#include + +#include "utils.h" // readFile + +#include + +namespace bbp { +namespace sonata { + +namespace fs = ghc::filesystem; + +namespace detail { + +using json = nlohmann::json; + +class CompartmentSets +{ + + public: + explicit CompartmentSets(const json& j) { + if (!j.is_object()) { + throw SonataError("Top level compartment_set must be an object"); + } + } + + static const fs::path& validate_path(const fs::path& path) { + if (!fs::exists(path)) { + throw SonataError(fmt::format("Path does not exist: {}", std::string(path))); + } + return path; + } + + explicit CompartmentSets(const fs::path& path) + : CompartmentSets(json::parse(std::ifstream(validate_path(path)))) {} + + static std::unique_ptr fromFile(const std::string& path_) { + fs::path path(path_); + return std::make_unique(path); + } + + explicit CompartmentSets(const std::string& content) + : CompartmentSets(json::parse(content)) {} + + std::string toJSON() const { + std::string ret{"{\n"}; + ret += "}"; + + return ret; + } +}; + +} // namespace detail +} // namespace sonata +} // namespace bbp \ No newline at end of file diff --git a/tests/data/config/compartment_sets.json b/tests/data/compartment_sets.json similarity index 100% rename from tests/data/config/compartment_sets.json rename to tests/data/compartment_sets.json diff --git a/tests/data/config/node_sets.json b/tests/data/node_sets.json similarity index 100% rename from tests/data/config/node_sets.json rename to tests/data/node_sets.json diff --git a/tests/test_node_sets.cpp b/tests/test_node_sets.cpp index 226bad9..e427775 100644 --- a/tests/test_node_sets.cpp +++ b/tests/test_node_sets.cpp @@ -299,7 +299,7 @@ TEST_CASE("NodeSet") { NodeSets ns1(j); CHECK(ns0.toJSON() == ns1.toJSON()); - auto ns = NodeSets::fromFile("./data/config/node_sets.json"); + auto ns = NodeSets::fromFile("./data/node_sets.json"); CHECK(ns.toJSON() == ns1.toJSON()); } From f7d3c0c8b23cdc6e567420f4bb8b0da91da120c1 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Tue, 27 May 2025 11:00:29 +0200 Subject: [PATCH 07/44] empty CompartmentSets. It should pass CI --- python/bindings.cpp | 1 + python/generated/docstrings.h | 2 ++ python/tests/test_config.py | 2 +- src/compartment_sets.cpp | 21 +++++++++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/python/bindings.cpp b/python/bindings.cpp index 15a2495..a466b18 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -127,6 +127,7 @@ py::object getDynamicsAttributeVectorWithDefault(const Population& obj, // create a macro to reduce repetition for docstrings #define DOC_NODESETS(x) DOC(bbp, sonata, NodeSets, x) +#define DOC_COMPARTMENTSETS(x) DOC(bbp, sonata, CompartmentSets, x) #define DOC_SEL(x) DOC(bbp, sonata, Selection, x) #define DOC_POP(x) DOC(bbp, sonata, Population, x) #define DOC_POP_NODE(x) DOC(bbp, sonata, NodePopulation, x) diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index a7d5f3f..2b2eb30 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -381,6 +381,8 @@ static const char *__doc_bbp_sonata_NodeSets_operator_assign = R"doc()doc"; static const char *__doc_bbp_sonata_NodeSets_toJSON = R"doc(Return the nodesets as a JSON string.)doc"; +static const char *__doc_bbp_sonata_CompartmentSets_toJSON = R"doc(Return the compartmentsets as a JSON string.)doc"; + static const char *__doc_bbp_sonata_NodeSets_update = R"doc(Update `this` to include all nodesets from `this` and `other`. diff --git a/python/tests/test_config.py b/python/tests/test_config.py index aec7700..5391620 100644 --- a/python/tests/test_config.py +++ b/python/tests/test_config.py @@ -16,7 +16,7 @@ def setUp(self): def test_basic(self): self.assertEqual(self.config.node_sets_path, - os.path.abspath(os.path.join(PATH, 'node_sets.json'))) + os.path.abspath(os.path.join(PATH, 'config/node_sets.json'))) self.assertEqual(self.config.node_populations, {'nodes-A', 'nodes-B'}) diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 9174521..c4e1526 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -60,5 +60,26 @@ class CompartmentSets }; } // namespace detail + +CompartmentSets::CompartmentSets(const std::string& content) + : impl_(new detail::CompartmentSets(content)) {} + +CompartmentSets::CompartmentSets(std::unique_ptr&& impl) + : impl_(std::move(impl)) {} + +CompartmentSets::CompartmentSets(CompartmentSets&&) noexcept = default; +CompartmentSets& CompartmentSets::operator=(CompartmentSets&&) noexcept = default; +CompartmentSets::~CompartmentSets() = default; + +CompartmentSets CompartmentSets::fromFile(const std::string& path) { + return CompartmentSets(detail::CompartmentSets::fromFile(path)); +} + + +std::string CompartmentSets::toJSON() const { + return impl_->toJSON(); +} + + } // namespace sonata } // namespace bbp \ No newline at end of file From 5b2bfb4462d1e9493d1c10e96711ec530e68b71c Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 28 May 2025 17:11:45 +0200 Subject: [PATCH 08/44] WIP --- include/bbp/sonata/compartment_sets.h | 106 +++++++++- python/bindings.cpp | 43 ++++ python/generated/docstrings.h | 24 ++- python/libsonata/__init__.py | 4 + python/tests/test_compartmentsets.py | 157 ++++++++++++++- src/compartment_sets.cpp | 269 ++++++++++++++++++++++++-- src/node_sets.cpp | 12 -- src/utils.h | 18 ++ test.py | 15 ++ tests/data/compartment_sets.json | 12 ++ 10 files changed, 626 insertions(+), 34 deletions(-) create mode 100644 test.py diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index dd4158a..a1df07f 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -5,19 +5,113 @@ namespace bbp { namespace sonata { namespace detail { + class CompartmentSetElement; +class CompartmentSet; class CompartmentSets; } // namespace detail +class SONATA_API CompartmentSetElement +{ + public: + /** + * Create CompartmentSetElement from JSON + * + * See also: + * TODO + * + * \param content is the JSON compartment_set value + * \throw if content cannot be parsed + */ + explicit CompartmentSetElement(const uint64_t gid, const std::string& section_name, + const uint64_t section_index, const double location); + explicit CompartmentSetElement(const std::string& content); + explicit CompartmentSetElement(std::unique_ptr&& impl); + CompartmentSetElement(CompartmentSetElement&&) noexcept; + CompartmentSetElement(const CompartmentSetElement& other) = delete; + CompartmentSetElement& operator=(CompartmentSetElement&&) noexcept; + ~CompartmentSetElement(); + + /** + * GID + */ + uint64_t gid() const; + /** + * Section name + */ + const std::string& sectionName() const; + /** + * Section index + */ + uint64_t sectionIndex() const; + + /** + * Location in the section + */ + double location() const; + + /** + * Return the nodesets as a JSON string. + */ + std::string toJSON() const; + + private: + std::unique_ptr impl_; +}; + + +class SONATA_API CompartmentSet +{ + public: + /** + * Create CompartmentSet from JSON + * + * See also: + * TODO + * + * \param content is the JSON compartment_set value + * \throw if content cannot be parsed + */ + explicit CompartmentSet(const std::string& content); + explicit CompartmentSet(std::unique_ptr&& impl); + CompartmentSet(CompartmentSet&&) noexcept; + CompartmentSet(const CompartmentSet& other) = delete; + CompartmentSet& operator=(CompartmentSet&&) noexcept; + ~CompartmentSet(); + + /** + * Population name + */ + const std::string& population() const; + + /** + * Get the Elements + */ + std::vector getElements(); + + /** + * Return the gids of the compartment set elements. + */ + std::vector gids() const; + + /** + * Return the nodesets as a JSON string. + */ + std::string toJSON() const; + + private: + std::unique_ptr impl_; +}; + class SONATA_API CompartmentSets { public: /** - * Create compartmentset from JSON + * Create CompartmentSets from JSON * * See also: * TODO * - * \param content is the JSON node_sets value + * \param content is the JSON compartment_sets value * \throw if content cannot be parsed */ explicit CompartmentSets(const std::string& content); @@ -30,11 +124,19 @@ class SONATA_API CompartmentSets /** Open a SONATA `Compartment sets` file from a path */ static CompartmentSets fromFile(const std::string& path); + /** + * Names of the node sets available + */ + std::set names() const; + /** * Return the nodesets as a JSON string. */ std::string toJSON() const; + // TODO + CompartmentSet getCompartmentSet(const std::string& name); + private: std::unique_ptr impl_; }; diff --git a/python/bindings.cpp b/python/bindings.cpp index a466b18..f826c43 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -127,6 +127,8 @@ py::object getDynamicsAttributeVectorWithDefault(const Population& obj, // create a macro to reduce repetition for docstrings #define DOC_NODESETS(x) DOC(bbp, sonata, NodeSets, x) +#define DOC_COMPARTMENTSETELEMENT(x) DOC(bbp, sonata, CompartmentSetElement, x) +#define DOC_COMPARTMENTSET(x) DOC(bbp, sonata, CompartmentSet, x) #define DOC_COMPARTMENTSETS(x) DOC(bbp, sonata, CompartmentSets, x) #define DOC_SEL(x) DOC(bbp, sonata, Selection, x) #define DOC_POP(x) DOC(bbp, sonata, Population, x) @@ -537,9 +539,50 @@ PYBIND11_MODULE(_libsonata, m) { .def("update", &NodeSets::update, "other"_a, DOC_NODESETS(update)) .def("toJSON", &NodeSets::toJSON, DOC_NODESETS(toJSON)); + py::class_(m, "CompartmentSetElement", "CompartmentSetElement") + .def(py::init()) + .def(py::init()) + .def_property_readonly("gid", &CompartmentSetElement::gid, DOC_COMPARTMENTSETELEMENT(gid)) + .def_property_readonly("section_name", &CompartmentSetElement::sectionName, DOC_COMPARTMENTSETELEMENT(sectionName)) + .def_property_readonly("section_index", &CompartmentSetElement::sectionIndex, DOC_COMPARTMENTSETELEMENT(sectionIndex)) + .def_property_readonly("location", &CompartmentSetElement::location, DOC_COMPARTMENTSETELEMENT(location)) + .def("__iter__", [](const CompartmentSetElement& self) { + return py::iter(py::make_tuple( + self.gid(), + self.sectionName(), + self.sectionIndex(), + self.location() + )); + }) + .def("__repr__", [](const CompartmentSetElement& self) { + return py::str("CompartmentSetElement({}, '{}', {}, {})") + .format(self.gid(), self.sectionName(), self.sectionIndex(), self.location()); + }) + .def("__str__", [](const CompartmentSetElement& self) { + return py::str(py::repr(py::cast(self))); // Delegates to __repr__ + }) + .def("toJSON", &CompartmentSetElement::toJSON, DOC_COMPARTMENTSETELEMENT(toJSON)); + + py::class_(m, "CompartmentSet", "CompartmentSet") + .def(py::init()) + .def_property_readonly("population", &CompartmentSet::population, DOC_COMPARTMENTSET(population)) + .def("elements", + [](CompartmentSet& self) { + return self.getElements(); + }, + py::return_value_policy::move, + DOC_COMPARTMENTSET(elements)) + .def("gids", + &CompartmentSet::gids, + py::return_value_policy::move, + DOC_COMPARTMENTSET(gids)) + .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)); + py::class_(m, "CompartmentSets", "CompartmentSets") .def(py::init()) + .def("compartment_set", &CompartmentSets::getCompartmentSet, DOC_COMPARTMENTSETS(getCompartmentSet)) .def_static("from_file", [](py::object path) { return CompartmentSets::fromFile(py::str(path)); }) + .def_property_readonly("names", &CompartmentSets::names, DOC_COMPARTMENTSETS(names)) .def("toJSON", &CompartmentSets::toJSON, DOC_COMPARTMENTSETS(toJSON)); py::class_(m, diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 2b2eb30..1aff22c 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -381,7 +381,29 @@ static const char *__doc_bbp_sonata_NodeSets_operator_assign = R"doc()doc"; static const char *__doc_bbp_sonata_NodeSets_toJSON = R"doc(Return the nodesets as a JSON string.)doc"; -static const char *__doc_bbp_sonata_CompartmentSets_toJSON = R"doc(Return the compartmentsets as a JSON string.)doc"; +static const char *__doc_bbp_sonata_CompartmentSetElement_gid = R"doc(GID)doc"; + +static const char *__doc_bbp_sonata_CompartmentSetElement_sectionName = R"doc(Section name)doc"; + +static const char *__doc_bbp_sonata_CompartmentSetElement_sectionIndex = R"doc(Section index)doc"; + +static const char *__doc_bbp_sonata_CompartmentSetElement_location = R"doc(Location along the section)doc"; + +static const char *__doc_bbp_sonata_CompartmentSetElement_toJSON = R"doc(Return the compartment set element as a JSON string.)doc"; + +static const char *__doc_bbp_sonata_CompartmentSet_elements = R"doc(Get the list of CompartmentSetElements.)doc"; + +static const char *__doc_bbp_sonata_CompartmentSet_gids = R"doc(Gids in the list of CompartmentSetElement.)doc"; + +static const char *__doc_bbp_sonata_CompartmentSet_toJSON = R"doc(Return the compartment set as a JSON string.)doc"; + +static const char *__doc_bbp_sonata_CompartmentSet_population = R"doc(Population name)doc"; + +static const char *__doc_bbp_sonata_CompartmentSets_toJSON = R"doc(Return the compartment sets as a JSON string.)doc"; + +static const char *__doc_bbp_sonata_CompartmentSets_names = R"doc(Names of the compartment sets available)doc"; + +static const char *__doc_bbp_sonata_CompartmentSets_getCompartmentSet = R"doc(Get compartment set)doc"; static const char *__doc_bbp_sonata_NodeSets_update = R"doc(Update `this` to include all nodesets from `this` and `other`. diff --git a/python/libsonata/__init__.py b/python/libsonata/__init__.py index 61ea915..9849323 100644 --- a/python/libsonata/__init__.py +++ b/python/libsonata/__init__.py @@ -14,6 +14,8 @@ ElementReportReader, NodePopulation, NodeSets, + CompartmentSetElement, + CompartmentSet, CompartmentSets, NodeStorage, Selection, @@ -39,6 +41,8 @@ "ElementReportReader", "NodePopulation", "NodeSets", + "CompartmentSetElement", + "CompartmentSet", "CompartmentSets", "NodeStorage", "Selection", diff --git a/python/tests/test_compartmentsets.py b/python/tests/test_compartmentsets.py index 39d0626..fe05728 100644 --- a/python/tests/test_compartmentsets.py +++ b/python/tests/test_compartmentsets.py @@ -7,6 +7,161 @@ SonataError, ) +PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../../tests/data') class TestCompartmentSetsFailure(unittest.TestCase): def test_CorrectStructure(self): - self.assertRaises(SonataError, CompartmentSets, "1") \ No newline at end of file + # Top level must be an object + self.assertRaises(SonataError, CompartmentSets, "1") + self.assertRaises(SonataError, CompartmentSets, '["array"]') + + # Each CompartmentSet must be an object with population and compartment_set keys + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": 1 }') + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": "string" }') + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": null }') + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": true }') + + def test_MissingPopulationOrCompartmentSet(self): + # Missing population key + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "compartment_set": [] } }') + # Missing compartment_set key + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop" } }') + + def test_InvalidPopulationType(self): + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": 123, "compartment_set": [] } }') + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": null, "compartment_set": [] } }') + + def test_InvalidCompartmentSetType(self): + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": "not an array" } }') + self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": 123 } }') + + def test_InvalidCompartmentSetElementStructure(self): + # Each compartment_set element must be an array of 4 elements [gid, section_name, section_index, location] + + # Not an array + self.assertRaises(SonataError, CompartmentSets, ''' + { + "CompartmentSet0": { + "population": "pop", + "compartment_set": [ 1 ] + } + } + ''') + + # Array with wrong size + self.assertRaises(SonataError, CompartmentSets, ''' + { + "CompartmentSet0": { + "population": "pop", + "compartment_set": [ [1, "sec", 0] ] + } + } + ''') + + # Wrong types inside element + self.assertRaises(SonataError, CompartmentSets, ''' + { + "CompartmentSet0": { + "population": "pop", + "compartment_set": [ ["not uint64", "sec", 0, 0.5] ] + } + } + ''') + + self.assertRaises(SonataError, CompartmentSets, ''' + { + "CompartmentSet0": { + "population": "pop", + "compartment_set": [ [1, 123, 0, 0.5] ] + } + } + ''') + + self.assertRaises(SonataError, CompartmentSets, ''' + { + "CompartmentSet0": { + "population": "pop", + "compartment_set": [ [1, "sec", "not uint64", 0.5] ] + } + } + ''') + + self.assertRaises(SonataError, CompartmentSets, ''' + { + "CompartmentSet0": { + "population": "pop", + "compartment_set": [ [1, "sec", 0, "not a number"] ] + } + } + ''') + + # Location out of bounds + self.assertRaises(SonataError, CompartmentSets, ''' + { + "CompartmentSet0": { + "population": "pop", + "compartment_set": [ [1, "sec", 0, -0.1] ] + } + } + ''') + + self.assertRaises(SonataError, CompartmentSets, ''' + { + "CompartmentSet0": { + "population": "pop", + "compartment_set": [ [1, "sec", 0, 1.1] ] + } + } + ''') + + def test_MissingFile(self): + self.assertRaises(SonataError, CompartmentSets.from_file, 'this/file/does/not/exist') + + +class TestCompartmentSet(unittest.TestCase): + def setUp(self): + self.compartment_sets = CompartmentSets.from_file(os.path.join(PATH, "compartment_sets.json")) + + def test_BasicCompartmentIdSelection(self): + pass + # sel = self.compartment_sets.materialize("CompartmentSet0", self.storage) + # self.assertEqual(sel, Selection(((0, 3),))) # Adapt expected selection + + # def test_InvalidSetName(self): + # with self.assertRaises(KeyError): + # self.compartment_sets.materialize("UnknownSet", self.storage) + + # def test_EmptySet(self): + # sel = self.compartment_sets.materialize("EmptySet", self.storage) + # self.assertEqual(sel, Selection([])) + + # def test_CompoundSet(self): + # j = { + # "SetA": { "compartment_id": [1, 2] }, + # "SetB": { "compartment_id": [3] }, + # "CompoundSet": ["SetA", "SetB"] + # } + # cs = CompartmentSets(json.dumps(j)) + # sel = cs.materialize("CompoundSet", self.storage) + # self.assertEqual(sel, Selection(((1, 2), (3, 4)))) # Adjust to real ranges + + # def test_toJSON_roundtrip(self): + # j = json.dumps({ + # "SetA": { "compartment_id": [0, 1] }, + # "SetB": { "compartment_id": [2] }, + # "CompoundSet": ["SetA", "SetB"] + # }) + # cs = CompartmentSets(j) + # new = cs.toJSON() + # self.assertEqual(cs.toJSON(), CompartmentSets(new).toJSON()) + + # def test_update_sets(self): + # cs1 = CompartmentSets(json.dumps({"SetA": { "compartment_id": [1] }})) + # cs2 = CompartmentSets(json.dumps({"SetB": { "compartment_id": [2] }})) + # dup = cs1.update(cs2) + # self.assertEqual(dup, set()) + # self.assertEqual(cs1.names, {"SetA", "SetB"}) + + # cs3 = CompartmentSets(json.dumps({"SetA": { "compartment_id": [1] }})) + # dup = cs1.update(cs3) + # self.assertEqual(dup, {"SetA"}) \ No newline at end of file diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index c4e1526..aa42e0f 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -1,19 +1,10 @@ -#include // std::find, std::transform -#include -#include -#include -#include -#include - #include "../extlib/filesystem.hpp" -#include -#include +#include #include "utils.h" // readFile #include - namespace bbp { namespace sonata { @@ -23,14 +14,163 @@ namespace detail { using json = nlohmann::json; +class CompartmentSetElement { + std::uint64_t gid_; + std::string section_name_; + std::uint64_t section_index_; + double location_; + +public: + + explicit CompartmentSetElement(uint64_t gid, + const std::string& section_name, + uint64_t section_index, + double location) + : gid_(gid), + section_name_(section_name), + section_index_(section_index), + location_(location) { + if (location < 0.0 || location > 1.0) { + throw SonataError(fmt::format("Location must be between 0 and 1 inclusive, got {}", location)); + } + } + + explicit CompartmentSetElement(const std::string& content) + : CompartmentSetElement(json::parse(content)) {} + + CompartmentSetElement(const nlohmann::json& j) { + if (!j.is_array() || j.size() != 4) { + throw SonataError("CompartmentSetElement must be an array of exactly 4 elements: [gid, \"section_name\", section_index, location]"); + } + + gid_ = get_uint64_or_throw(j[0]); + + if (!j[1].is_string()) { + throw SonataError("Second element (section_name) must be a string"); + } + section_name_ = j[1].get(); + + section_index_ = get_uint64_or_throw(j[2]); + + if (!j[3].is_number()) { + throw SonataError("Fourth element (location) must be a number"); + } + const double location = j[3].get(); + if (location < 0.0 || location > 1.0) { + throw SonataError(fmt::format("Location must be between 0 and 1 inclusive, got {}", location)); + } + location_ = location; + } + + uint64_t gid() const { + return gid_; + } + + const std::string& sectionName() const { + return section_name_; + } + + uint64_t sectionIndex() const { + return section_index_; + } + + double location() const { + return location_; + } + + nlohmann::json to_json() const { + return nlohmann::json::array({gid_, section_name_, section_index_, location_}); + } +}; + + +class CompartmentSet { + std::string population_; + std::vector compartment_set_elements_; + +public: + + explicit CompartmentSet(const std::string& content) + : CompartmentSet(json::parse(content)) {} + + explicit CompartmentSet(const nlohmann::json& j) { + if (!j.is_object()) { + throw SonataError("CompartmentSet must be an object"); + } + + if (j.contains("population")) { + if (!j.at("population").is_string()) { + throw SonataError("'population' must be a string"); + } + population_ = j.at("population").get(); + } else { + throw SonataError("CompartmentSet must contain 'population' key"); + } + + if (j.contains("compartment_set")) { + if (!j.at("compartment_set").is_array()) { + throw SonataError("'compartment_set' must be an array"); + } + + for (const auto& el : j.at("compartment_set")) { + compartment_set_elements_.emplace_back(el); + } + } else { + throw SonataError("CompartmentSet must contain 'compartment_set' key"); + } + } + + std::vector gids() const { + std::vector result; + std::unordered_set seen; + + result.reserve(compartment_set_elements_.size()); + for (const auto& elem : compartment_set_elements_) { + uint64_t id = elem.gid(); + if (seen.insert(id).second) { // insert returns {iterator, bool} + result.push_back(id); + } + } + return result; + } + + const std::string& population() const { + return population_; + } + + std::vector& getElements() { + return compartment_set_elements_; + } + + const std::vector& getElements() const { + return compartment_set_elements_; + } + + nlohmann::json to_json() const { + nlohmann::json j; + j["population"] = population_; + + j["compartment_set"] = nlohmann::json::array(); + for (const auto& elem : compartment_set_elements_) { + j["compartment_set"].push_back(elem.to_json()); + } + + return j; + } +}; class CompartmentSets { +std::map compartment_sets_; + public: explicit CompartmentSets(const json& j) { if (!j.is_object()) { throw SonataError("Top level compartment_set must be an object"); } + for (const auto& el : j.items()) { + compartment_sets_.emplace(el.key(), el.value()); + } } static const fs::path& validate_path(const fs::path& path) { @@ -40,27 +180,113 @@ class CompartmentSets return path; } - explicit CompartmentSets(const fs::path& path) - : CompartmentSets(json::parse(std::ifstream(validate_path(path)))) {} - static std::unique_ptr fromFile(const std::string& path_) { fs::path path(path_); return std::make_unique(path); } + explicit CompartmentSets(const fs::path& path) + : CompartmentSets(json::parse(std::ifstream(validate_path(path)))) {} + explicit CompartmentSets(const std::string& content) : CompartmentSets(json::parse(content)) {} - std::string toJSON() const { - std::string ret{"{\n"}; - ret += "}"; + std::set names() const { + return getMapKeys(compartment_sets_); + } - return ret; + CompartmentSet getCompartmentSet(const std::string& name) const { + auto it = compartment_sets_.find(name); + if (it == compartment_sets_.end()) { + throw SonataError(fmt::format("CompartmentSet '{}' not found", name)); + } + return it->second; + } + + nlohmann::json to_json() const { + nlohmann::json j; + for (const auto& entry : compartment_sets_) { + j[entry.first] = entry.second.to_json(); + } + return j; } }; + + } // namespace detail +// CompartmentSetElement python API + +CompartmentSetElement::CompartmentSetElement(const uint64_t gid, + const std::string& section_name, + const uint64_t section_index, + const double location) + : impl_(new detail::CompartmentSetElement(gid, section_name, section_index, location)) {} + +CompartmentSetElement::CompartmentSetElement(const std::string& content) + : impl_(new detail::CompartmentSetElement(content)) {} + +CompartmentSetElement::CompartmentSetElement(std::unique_ptr&& impl) + : impl_(std::move(impl)) {} + +CompartmentSetElement::CompartmentSetElement(CompartmentSetElement&&) noexcept = default; +CompartmentSetElement& CompartmentSetElement::operator=(CompartmentSetElement&&) noexcept = default; +CompartmentSetElement::~CompartmentSetElement() = default; + +uint64_t CompartmentSetElement::gid() const { + return impl_->gid(); +} +const std::string& CompartmentSetElement::sectionName() const { + return impl_->sectionName(); +} +uint64_t CompartmentSetElement::sectionIndex() const { + return impl_->sectionIndex(); +} + +double CompartmentSetElement::location() const { + return impl_->location(); +} + +std::string CompartmentSetElement::toJSON() const { + return impl_->to_json().dump(4); // Pretty print with 4 spaces +} + +// CompartmentSet python API + +CompartmentSet::CompartmentSet(const std::string& content) + : impl_(new detail::CompartmentSet(content)) {} + +CompartmentSet::CompartmentSet(std::unique_ptr&& impl) + : impl_(std::move(impl)) {} + +CompartmentSet::CompartmentSet(CompartmentSet&&) noexcept = default; +CompartmentSet& CompartmentSet::operator=(CompartmentSet&&) noexcept = default; +CompartmentSet::~CompartmentSet() = default; + +const std::string& CompartmentSet::population() const { + return impl_->population(); +} + +std::vector CompartmentSet::getElements() { + std::vector view; + view.reserve(impl_->getElements().size()); + for (auto& el : impl_->getElements()) { + view.emplace_back(std::make_unique(el)); + } + return view; +} + +std::vector CompartmentSet::gids() const { + return impl_->gids(); +} + +std::string CompartmentSet::toJSON() const { + return impl_->to_json().dump(4); // Pretty print with 4 spaces +} + +// CompartmentSets python API + CompartmentSets::CompartmentSets(const std::string& content) : impl_(new detail::CompartmentSets(content)) {} @@ -75,9 +301,16 @@ CompartmentSets CompartmentSets::fromFile(const std::string& path) { return CompartmentSets(detail::CompartmentSets::fromFile(path)); } +std::set CompartmentSets::names() const { + return impl_->names(); +} + +CompartmentSet CompartmentSets::getCompartmentSet(const std::string& name) { + return CompartmentSet(std::make_unique(impl_->getCompartmentSet(name))); +} std::string CompartmentSets::toJSON() const { - return impl_->toJSON(); + return impl_->to_json().dump(4); // Pretty print with 4 spaces } diff --git a/src/node_sets.cpp b/src/node_sets.cpp index 2182c45..7a494f3 100644 --- a/src/node_sets.cpp +++ b/src/node_sets.cpp @@ -470,18 +470,6 @@ int64_t get_int64_or_throw(const json& el) { return static_cast(v); } -uint64_t get_uint64_or_throw(const json& el) { - auto v = el.get(); - if (v < 0) { - throw SonataError(fmt::format("expected unsigned integer, got {}", v)); - } - - if (std::floor(v) != v) { - throw SonataError(fmt::format("expected integer, got float {}", v)); - } - return static_cast(v); -} - NodeSetRulePtr _dispatch_node(const std::string& attribute, const json& value) { if (value.is_number()) { if (attribute == "population") { diff --git a/src/utils.h b/src/utils.h index df4e3f2..02c29cd 100644 --- a/src/utils.h +++ b/src/utils.h @@ -16,6 +16,8 @@ #include #include +#include +#include std::string readFile(const std::string& path); @@ -44,5 +46,21 @@ std::set getMapKeys(const T& map) { }); return ret; } + +using json = nlohmann::json; +inline uint64_t get_uint64_or_throw(const json& el) { + if (!el.is_number()) { + throw SonataError(fmt::format("expected unsigned integer, got {}", el.dump())); + } + auto v = el.get(); + if (v < 0) { + throw SonataError(fmt::format("expected unsigned integer, got {}", v)); + } + + if (std::floor(v) != v) { + throw SonataError(fmt::format("expected integer, got float {}", v)); + } + return static_cast(v); +} } // namespace sonata } // namespace bbp diff --git a/test.py b/test.py new file mode 100644 index 0000000..13055f5 --- /dev/null +++ b/test.py @@ -0,0 +1,15 @@ +from libsonata import ( + CompartmentSetElement, + CompartmentSets, + CompartmentSet, + SonataError, +) + +def inspect(v): + print(v, type(v)) + for i in dir(v): + # if not i.startswith('__'): + print(f' {i} = {getattr(v, i)}') + +a = CompartmentSets('{ "CompartmentSet0": { "population": "pop0", "compartment_set": [[0, "dend", 10, 0.2], [2, "dend", 11, 0.2], [0, "dend", 11, 0.2], [1, "dend", 11, 0.2]] } }') +print(a.compartment_set("CompartmentSet0").gids()) \ No newline at end of file diff --git a/tests/data/compartment_sets.json b/tests/data/compartment_sets.json index e69de29..f7aa0ba 100644 --- a/tests/data/compartment_sets.json +++ b/tests/data/compartment_sets.json @@ -0,0 +1,12 @@ + { + "example_compartment_set": { + "population": "S1nonbarrel_neurons", + "compartment_set": [ + [0, "dend", 10, 0.1], + [0, "dend", 10, 0.2], + [0, "dend", 10, 0.1], + [2, "axon", 3, 0.1], + [3, "dend", 6, 0.3] + ] + } + } \ No newline at end of file From 9475c90fe58ae56c0cae46b006c2ad81fa0b5ca4 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Fri, 30 May 2025 17:08:10 +0200 Subject: [PATCH 09/44] before clang-format --- include/bbp/sonata/compartment_sets.h | 48 ++--- include/bbp/sonata/selection.h | 7 + python/bindings.cpp | 54 +++--- python/generated/docstrings.h | 14 +- python/libsonata/__init__.py | 4 +- python/tests/test_compartmentsets.py | 75 ++------ src/compartment_sets.cpp | 250 ++++++++++++++++---------- src/node_sets.cpp | 8 - src/selection.cpp | 9 + src/utils.h | 13 ++ test.py | 8 +- tests/CMakeLists.txt | 10 +- tests/data/compartment_sets.json | 10 +- tests/test_compartment_sets.cpp | 137 ++++++++++++++ 14 files changed, 404 insertions(+), 243 deletions(-) create mode 100644 tests/test_compartment_sets.cpp diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index a1df07f..ee76f28 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -5,16 +5,16 @@ namespace bbp { namespace sonata { namespace detail { - class CompartmentSetElement; + class CompartmentLocation; class CompartmentSet; class CompartmentSets; } // namespace detail -class SONATA_API CompartmentSetElement +class SONATA_API CompartmentLocation { public: /** - * Create CompartmentSetElement from JSON + * Create CompartmentLocation from JSON * * See also: * TODO @@ -22,32 +22,30 @@ class SONATA_API CompartmentSetElement * \param content is the JSON compartment_set value * \throw if content cannot be parsed */ - explicit CompartmentSetElement(const uint64_t gid, const std::string& section_name, - const uint64_t section_index, const double location); - explicit CompartmentSetElement(const std::string& content); - explicit CompartmentSetElement(std::unique_ptr&& impl); - CompartmentSetElement(CompartmentSetElement&&) noexcept; - CompartmentSetElement(const CompartmentSetElement& other) = delete; - CompartmentSetElement& operator=(CompartmentSetElement&&) noexcept; - ~CompartmentSetElement(); + explicit CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset); + explicit CompartmentLocation(const std::string& content); + explicit CompartmentLocation(std::unique_ptr&& impl); + CompartmentLocation(CompartmentLocation&&) noexcept; + CompartmentLocation(const CompartmentLocation& other) = delete; + CompartmentLocation& operator=(CompartmentLocation&&) noexcept; + bool operator==(const CompartmentLocation& other) const noexcept; + ~CompartmentLocation(); /** * GID */ uint64_t gid() const; + /** - * Section name - */ - const std::string& sectionName() const; - /** - * Section index + * Absolute section index. Progressive index that uniquely identifies the section. + * There is a mapping between neuron section names (i.e. dend[10]) and this index. */ - uint64_t sectionIndex() const; + uint64_t sectionIdx() const; /** - * Location in the section + * Offset of the compartment along the section. */ - double location() const; + double offset() const; /** * Return the nodesets as a JSON string. @@ -55,7 +53,7 @@ class SONATA_API CompartmentSetElement std::string toJSON() const; private: - std::unique_ptr impl_; + std::unique_ptr impl_; }; @@ -84,14 +82,14 @@ class SONATA_API CompartmentSet const std::string& population() const; /** - * Get the Elements + * Get the CompartmentLocations. */ - std::vector getElements(); + std::vector getCompartmentLocations(const Selection& selection = Selection({})) const; /** - * Return the gids of the compartment set elements. + * Return the gids of the compartment locations. */ - std::vector gids() const; + Selection gids() const; /** * Return the nodesets as a JSON string. @@ -120,6 +118,8 @@ class SONATA_API CompartmentSets CompartmentSets(const CompartmentSets& other) = delete; CompartmentSets& operator=(CompartmentSets&&) noexcept; ~CompartmentSets(); + size_t size() const; + bool contains(const std::string& name) const; /** Open a SONATA `Compartment sets` file from a path */ static CompartmentSets fromFile(const std::string& path); diff --git a/include/bbp/sonata/selection.h b/include/bbp/sonata/selection.h index cca98f7..cc5de9b 100644 --- a/include/bbp/sonata/selection.h +++ b/include/bbp/sonata/selection.h @@ -40,6 +40,13 @@ class SONATA_API Selection bool empty() const; + /** + * Check if Selection contains a given GID (binary search) + * @param gid is the GID to check + * @return true if Selection contains gid, false otherwise + */ + bool contains(Value gid) const; + private: Ranges ranges_; }; diff --git a/python/bindings.cpp b/python/bindings.cpp index f826c43..c4dcd58 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -127,7 +127,7 @@ py::object getDynamicsAttributeVectorWithDefault(const Population& obj, // create a macro to reduce repetition for docstrings #define DOC_NODESETS(x) DOC(bbp, sonata, NodeSets, x) -#define DOC_COMPARTMENTSETELEMENT(x) DOC(bbp, sonata, CompartmentSetElement, x) +#define DOC_COMPARTMENTLOCATION(x) DOC(bbp, sonata, CompartmentLocation, x) #define DOC_COMPARTMENTSET(x) DOC(bbp, sonata, CompartmentSet, x) #define DOC_COMPARTMENTSETS(x) DOC(bbp, sonata, CompartmentSets, x) #define DOC_SEL(x) DOC(bbp, sonata, Selection, x) @@ -470,6 +470,9 @@ PYBIND11_MODULE(_libsonata, m) { .def( "flatten", [](Selection& obj) { return asArray(obj.flatten()); }, DOC_SEL(flatten)) .def_property_readonly("flat_size", &Selection::flatSize, DOC_SEL(flatSize)) + .def("__contains__", [](const Selection& sel, uint64_t gid) { + return sel.contains(gid); + }, "Check if a GID is contained in the selection") .def( "__bool__", [](const Selection& obj) { return !obj.empty(); }, @@ -539,47 +542,46 @@ PYBIND11_MODULE(_libsonata, m) { .def("update", &NodeSets::update, "other"_a, DOC_NODESETS(update)) .def("toJSON", &NodeSets::toJSON, DOC_NODESETS(toJSON)); - py::class_(m, "CompartmentSetElement", "CompartmentSetElement") + py::class_(m, "CompartmentLocation", "CompartmentLocation") .def(py::init()) - .def(py::init()) - .def_property_readonly("gid", &CompartmentSetElement::gid, DOC_COMPARTMENTSETELEMENT(gid)) - .def_property_readonly("section_name", &CompartmentSetElement::sectionName, DOC_COMPARTMENTSETELEMENT(sectionName)) - .def_property_readonly("section_index", &CompartmentSetElement::sectionIndex, DOC_COMPARTMENTSETELEMENT(sectionIndex)) - .def_property_readonly("location", &CompartmentSetElement::location, DOC_COMPARTMENTSETELEMENT(location)) - .def("__iter__", [](const CompartmentSetElement& self) { + .def(py::init()) + .def_property_readonly("gid", &CompartmentLocation::gid, DOC_COMPARTMENTLOCATION(gid)) + .def_property_readonly("section_idx", &CompartmentLocation::sectionIdx, DOC_COMPARTMENTLOCATION(sectionIdx)) + .def_property_readonly("offset", &CompartmentLocation::offset, DOC_COMPARTMENTLOCATION(offset)) + .def("__iter__", [](const CompartmentLocation& self) { return py::iter(py::make_tuple( self.gid(), - self.sectionName(), - self.sectionIndex(), - self.location() + self.sectionIdx(), + self.offset() )); }) - .def("__repr__", [](const CompartmentSetElement& self) { - return py::str("CompartmentSetElement({}, '{}', {}, {})") - .format(self.gid(), self.sectionName(), self.sectionIndex(), self.location()); + .def("__repr__", [](const CompartmentLocation& self) { + return py::str("CompartmentLocation({}, {}, {})") + .format(self.gid(), self.sectionIdx(), self.offset()); }) - .def("__str__", [](const CompartmentSetElement& self) { + .def("__str__", [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); // Delegates to __repr__ }) - .def("toJSON", &CompartmentSetElement::toJSON, DOC_COMPARTMENTSETELEMENT(toJSON)); + .def("__eq__", &CompartmentLocation::operator==) + .def("toJSON", &CompartmentLocation::toJSON, DOC_COMPARTMENTLOCATION(toJSON)); py::class_(m, "CompartmentSet", "CompartmentSet") .def(py::init()) .def_property_readonly("population", &CompartmentSet::population, DOC_COMPARTMENTSET(population)) - .def("elements", - [](CompartmentSet& self) { - return self.getElements(); - }, - py::return_value_policy::move, - DOC_COMPARTMENTSET(elements)) - .def("gids", - &CompartmentSet::gids, - py::return_value_policy::move, - DOC_COMPARTMENTSET(gids)) + .def("compartment_locations", + [](const CompartmentSet& self, const Selection& sel) { + return self.getCompartmentLocations(sel); + }, + py::arg("selection") = Selection({}), // provide default arg here + DOC_COMPARTMENTSET(getCompartmentLocations) + ) + .def("gids", &CompartmentSet::gids, DOC_COMPARTMENTSET(gids)) .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)); py::class_(m, "CompartmentSets", "CompartmentSets") .def(py::init()) + .def("__contains__", &CompartmentSets::contains) + .def("__len__", &CompartmentSets::size) .def("compartment_set", &CompartmentSets::getCompartmentSet, DOC_COMPARTMENTSETS(getCompartmentSet)) .def_static("from_file", [](py::object path) { return CompartmentSets::fromFile(py::str(path)); }) .def_property_readonly("names", &CompartmentSets::names, DOC_COMPARTMENTSETS(names)) diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 1aff22c..7e967b4 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -381,19 +381,17 @@ static const char *__doc_bbp_sonata_NodeSets_operator_assign = R"doc()doc"; static const char *__doc_bbp_sonata_NodeSets_toJSON = R"doc(Return the nodesets as a JSON string.)doc"; -static const char *__doc_bbp_sonata_CompartmentSetElement_gid = R"doc(GID)doc"; +static const char *__doc_bbp_sonata_CompartmentLocation_gid = R"doc(GID)doc"; -static const char *__doc_bbp_sonata_CompartmentSetElement_sectionName = R"doc(Section name)doc"; +static const char *__doc_bbp_sonata_CompartmentLocation_sectionIdx = R"doc(Absolute section index. Progressive index that uniquely identifies the section. There is a mapping between neuron section names (i.e. dend[10]) and this index.)doc"; -static const char *__doc_bbp_sonata_CompartmentSetElement_sectionIndex = R"doc(Section index)doc"; +static const char *__doc_bbp_sonata_CompartmentLocation_offset = R"doc(Offset of the compartment along the section)doc"; -static const char *__doc_bbp_sonata_CompartmentSetElement_location = R"doc(Location along the section)doc"; +static const char *__doc_bbp_sonata_CompartmentLocation_toJSON = R"doc(Return the compartment set element as a JSON string.)doc"; -static const char *__doc_bbp_sonata_CompartmentSetElement_toJSON = R"doc(Return the compartment set element as a JSON string.)doc"; +static const char *__doc_bbp_sonata_CompartmentSet_getCompartmentLocations = R"doc(Get the list of CompartmentLocations.)doc"; -static const char *__doc_bbp_sonata_CompartmentSet_elements = R"doc(Get the list of CompartmentSetElements.)doc"; - -static const char *__doc_bbp_sonata_CompartmentSet_gids = R"doc(Gids in the list of CompartmentSetElement.)doc"; +static const char *__doc_bbp_sonata_CompartmentSet_gids = R"doc(Gids in the list of CompartmentLocation.)doc"; static const char *__doc_bbp_sonata_CompartmentSet_toJSON = R"doc(Return the compartment set as a JSON string.)doc"; diff --git a/python/libsonata/__init__.py b/python/libsonata/__init__.py index 9849323..93fce0a 100644 --- a/python/libsonata/__init__.py +++ b/python/libsonata/__init__.py @@ -14,7 +14,7 @@ ElementReportReader, NodePopulation, NodeSets, - CompartmentSetElement, + CompartmentLocation, CompartmentSet, CompartmentSets, NodeStorage, @@ -41,7 +41,7 @@ "ElementReportReader", "NodePopulation", "NodeSets", - "CompartmentSetElement", + "CompartmentLocation", "CompartmentSet", "CompartmentSets", "NodeStorage", diff --git a/python/tests/test_compartmentsets.py b/python/tests/test_compartmentsets.py index fe05728..dcb7352 100644 --- a/python/tests/test_compartmentsets.py +++ b/python/tests/test_compartmentsets.py @@ -35,7 +35,7 @@ def test_InvalidCompartmentSetType(self): self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": "not an array" } }') self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": 123 } }') - def test_InvalidCompartmentSetElementStructure(self): + def test_InvalidCompartmentLocationStructure(self): # Each compartment_set element must be an array of 4 elements [gid, section_name, section_index, location] # Not an array @@ -53,7 +53,7 @@ def test_InvalidCompartmentSetElementStructure(self): { "CompartmentSet0": { "population": "pop", - "compartment_set": [ [1, "sec", 0] ] + "compartment_set": [ [1, 0] ] } } ''') @@ -63,25 +63,17 @@ def test_InvalidCompartmentSetElementStructure(self): { "CompartmentSet0": { "population": "pop", - "compartment_set": [ ["not uint64", "sec", 0, 0.5] ] + "compartment_set": [ ["not uint64", 0, 0.5] ] } } ''') - self.assertRaises(SonataError, CompartmentSets, ''' - { - "CompartmentSet0": { - "population": "pop", - "compartment_set": [ [1, 123, 0, 0.5] ] - } - } - ''') self.assertRaises(SonataError, CompartmentSets, ''' { "CompartmentSet0": { "population": "pop", - "compartment_set": [ [1, "sec", "not uint64", 0.5] ] + "compartment_set": [ [1, "not uint64", 0.5] ] } } ''') @@ -90,7 +82,7 @@ def test_InvalidCompartmentSetElementStructure(self): { "CompartmentSet0": { "population": "pop", - "compartment_set": [ [1, "sec", 0, "not a number"] ] + "compartment_set": [ [1, 0, "not a number"] ] } } ''') @@ -100,7 +92,7 @@ def test_InvalidCompartmentSetElementStructure(self): { "CompartmentSet0": { "population": "pop", - "compartment_set": [ [1, "sec", 0, -0.1] ] + "compartment_set": [ [1, 0, -0.1] ] } } ''') @@ -109,7 +101,7 @@ def test_InvalidCompartmentSetElementStructure(self): { "CompartmentSet0": { "population": "pop", - "compartment_set": [ [1, "sec", 0, 1.1] ] + "compartment_set": [ [1, 0, 1.1] ] } } ''') @@ -118,50 +110,9 @@ def test_MissingFile(self): self.assertRaises(SonataError, CompartmentSets.from_file, 'this/file/does/not/exist') -class TestCompartmentSet(unittest.TestCase): - def setUp(self): - self.compartment_sets = CompartmentSets.from_file(os.path.join(PATH, "compartment_sets.json")) - - def test_BasicCompartmentIdSelection(self): - pass - # sel = self.compartment_sets.materialize("CompartmentSet0", self.storage) - # self.assertEqual(sel, Selection(((0, 3),))) # Adapt expected selection - - # def test_InvalidSetName(self): - # with self.assertRaises(KeyError): - # self.compartment_sets.materialize("UnknownSet", self.storage) - - # def test_EmptySet(self): - # sel = self.compartment_sets.materialize("EmptySet", self.storage) - # self.assertEqual(sel, Selection([])) - - # def test_CompoundSet(self): - # j = { - # "SetA": { "compartment_id": [1, 2] }, - # "SetB": { "compartment_id": [3] }, - # "CompoundSet": ["SetA", "SetB"] - # } - # cs = CompartmentSets(json.dumps(j)) - # sel = cs.materialize("CompoundSet", self.storage) - # self.assertEqual(sel, Selection(((1, 2), (3, 4)))) # Adjust to real ranges - - # def test_toJSON_roundtrip(self): - # j = json.dumps({ - # "SetA": { "compartment_id": [0, 1] }, - # "SetB": { "compartment_id": [2] }, - # "CompoundSet": ["SetA", "SetB"] - # }) - # cs = CompartmentSets(j) - # new = cs.toJSON() - # self.assertEqual(cs.toJSON(), CompartmentSets(new).toJSON()) - - # def test_update_sets(self): - # cs1 = CompartmentSets(json.dumps({"SetA": { "compartment_id": [1] }})) - # cs2 = CompartmentSets(json.dumps({"SetB": { "compartment_id": [2] }})) - # dup = cs1.update(cs2) - # self.assertEqual(dup, set()) - # self.assertEqual(cs1.names, {"SetA", "SetB"}) - - # cs3 = CompartmentSets(json.dumps({"SetA": { "compartment_id": [1] }})) - # dup = cs1.update(cs3) - # self.assertEqual(dup, {"SetA"}) \ No newline at end of file +# class TestCompartmentSet(unittest.TestCase): +# def setUp(self): +# self.compartment_sets = CompartmentSets.from_file(os.path.join(PATH, "compartment_sets.json")) + +# def test_BasicCompartmentIdSelection(self): +# pass diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index aa42e0f..5ae2ec0 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -1,6 +1,8 @@ #include "../extlib/filesystem.hpp" #include +#include +#include #include "utils.h" // readFile @@ -14,79 +16,105 @@ namespace detail { using json = nlohmann::json; -class CompartmentSetElement { +class CompartmentLocation { std::uint64_t gid_; - std::string section_name_; - std::uint64_t section_index_; - double location_; + std::uint64_t section_idx_; + double offset_; -public: - - explicit CompartmentSetElement(uint64_t gid, - const std::string& section_name, - uint64_t section_index, - double location) - : gid_(gid), - section_name_(section_name), - section_index_(section_index), - location_(location) { - if (location < 0.0 || location > 1.0) { - throw SonataError(fmt::format("Location must be between 0 and 1 inclusive, got {}", location)); + void setGid(int64_t gid) { + if (gid < 0) { + throw SonataError(fmt::format("GID must be non-negative, got {}", gid)); + } + gid_ = static_cast(gid); + } + void setSectionIdx(int64_t section_idx) { + if (section_idx < 0) { + throw SonataError(fmt::format("Section index must be non-negative, got {}", section_idx)); + } + section_idx_ = static_cast(section_idx); + } + void setOffset(double offset) { + if (offset < 0.0 || offset > 1.0) { + throw SonataError(fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); } + offset_ = offset; } - explicit CompartmentSetElement(const std::string& content) - : CompartmentSetElement(json::parse(content)) {} +public: + static constexpr double offsetTolerance = 1e-4; + static constexpr double offsetToleranceInv = 1.0 / offsetTolerance; - CompartmentSetElement(const nlohmann::json& j) { - if (!j.is_array() || j.size() != 4) { - throw SonataError("CompartmentSetElement must be an array of exactly 4 elements: [gid, \"section_name\", section_index, location]"); - } + explicit CompartmentLocation(int64_t gid, int64_t section_idx, double offset) { + setGid(gid); + setSectionIdx(section_idx); + setOffset(offset); + } - gid_ = get_uint64_or_throw(j[0]); + explicit CompartmentLocation(const std::string& content) + : CompartmentLocation(json::parse(content)) {} - if (!j[1].is_string()) { - throw SonataError("Second element (section_name) must be a string"); + CompartmentLocation(const nlohmann::json& j) { + if (!j.is_array() || j.size() != 3) { + throw SonataError("CompartmentLocation must be an array of exactly 3 elements: [gid, section_idx, offset]"); } - section_name_ = j[1].get(); - section_index_ = get_uint64_or_throw(j[2]); + setGid(get_uint64_or_throw(j[0])); + setSectionIdx(get_uint64_or_throw(j[1])); - if (!j[3].is_number()) { - throw SonataError("Fourth element (location) must be a number"); + if (!j[2].is_number()) { + throw SonataError("Fourth element (offset) must be a number"); } - const double location = j[3].get(); - if (location < 0.0 || location > 1.0) { - throw SonataError(fmt::format("Location must be between 0 and 1 inclusive, got {}", location)); - } - location_ = location; + setOffset(j[2].get()); } uint64_t gid() const { return gid_; } - const std::string& sectionName() const { - return section_name_; + uint64_t sectionIdx() const { + return section_idx_; } - uint64_t sectionIndex() const { - return section_index_; + double offset() const { + return offset_; } - double location() const { - return location_; + nlohmann::json to_json() const { + return nlohmann::json::array({gid_, section_idx_, offset_}); } - nlohmann::json to_json() const { - return nlohmann::json::array({gid_, section_name_, section_index_, location_}); + bool operator==(const CompartmentLocation& other) const { + return gid_ == other.gid_ + && section_idx_ == other.section_idx_ + && std::abs(offset_ - other.offset_) < offsetTolerance; } }; +// Custom hash for CompartmentLocation +struct CompartmentLocationHash { + std::size_t operator()(const CompartmentLocation& loc) const noexcept { + std::size_t h1 = std::hash{}(loc.gid()); + std::size_t h2 = std::hash{}(loc.sectionIdx()); + + // Quantize offset to 4 decimal places + double offset = loc.offset(); + uint64_t quantized_offset = static_cast(std::round(offset * CompartmentLocation::offsetToleranceInv)); + + std::size_t h3 = std::hash{}(quantized_offset); + + // Combine hashes (boost style) + std::size_t seed = 0; + seed ^= h1 + 0x9e3779b9 + (seed << 6) + (seed >> 2); + seed ^= h2 + 0x9e3779b9 + (seed << 6) + (seed >> 2); + seed ^= h3 + 0x9e3779b9 + (seed << 6) + (seed >> 2); + + return seed; + } +}; class CompartmentSet { std::string population_; - std::vector compartment_set_elements_; + std::vector compartment_locations_; public: @@ -98,52 +126,65 @@ class CompartmentSet { throw SonataError("CompartmentSet must be an object"); } - if (j.contains("population")) { - if (!j.at("population").is_string()) { - throw SonataError("'population' must be a string"); - } - population_ = j.at("population").get(); - } else { - throw SonataError("CompartmentSet must contain 'population' key"); + // Extract and check 'population' key once + auto pop_it = j.find("population"); + if (pop_it == j.end() || !pop_it->is_string()) { + throw SonataError("CompartmentSet must contain 'population' key of string type"); } + population_ = pop_it->get(); - if (j.contains("compartment_set")) { - if (!j.at("compartment_set").is_array()) { - throw SonataError("'compartment_set' must be an array"); - } + // Extract and check 'compartment_set' key once + auto comp_it = j.find("compartment_set"); + if (comp_it == j.end() || !comp_it->is_array()) { + throw SonataError("CompartmentSet must contain 'compartment_set' key of array type"); + } - for (const auto& el : j.at("compartment_set")) { - compartment_set_elements_.emplace_back(el); - } - } else { - throw SonataError("CompartmentSet must contain 'compartment_set' key"); + compartment_locations_.reserve(comp_it->size()); + for (const auto& el : *comp_it) { + compartment_locations_.emplace_back(el); } + compartment_locations_.shrink_to_fit(); } - std::vector gids() const { + + Selection gids() const { std::vector result; std::unordered_set seen; - result.reserve(compartment_set_elements_.size()); - for (const auto& elem : compartment_set_elements_) { + result.reserve(compartment_locations_.size()); + for (const auto& elem : compartment_locations_) { uint64_t id = elem.gid(); if (seen.insert(id).second) { // insert returns {iterator, bool} result.push_back(id); } } - return result; + sort(result.begin(), result.end()); + return Selection::fromValues(result.begin(), result.end()); } const std::string& population() const { return population_; } - std::vector& getElements() { - return compartment_set_elements_; - } + std::vector> + getCompartmentLocations(const Selection& selection) const { + std::vector> result; + result.reserve(compartment_locations_.size()); - const std::vector& getElements() const { - return compartment_set_elements_; + if (selection.empty()) { + for (const auto& el : compartment_locations_) { + result.emplace_back(std::make_unique(el)); + } + } else { + for (const auto& el : compartment_locations_) { + if (selection.contains(el.gid())) { + result.emplace_back(std::make_unique(el)); + } + } + } + result.shrink_to_fit(); + + return result; } nlohmann::json to_json() const { @@ -151,7 +192,7 @@ class CompartmentSet { j["population"] = population_; j["compartment_set"] = nlohmann::json::array(); - for (const auto& elem : compartment_set_elements_) { + for (const auto& elem : compartment_locations_) { j["compartment_set"].push_back(elem.to_json()); } @@ -191,6 +232,13 @@ std::map compartment_sets_; explicit CompartmentSets(const std::string& content) : CompartmentSets(json::parse(content)) {} + size_t size() const { + return compartment_sets_.size(); + } + bool contains(const std::string& name) const { + return compartment_sets_.find(name) != compartment_sets_.end(); + } + std::set names() const { return getMapKeys(compartment_sets_); } @@ -216,39 +264,40 @@ std::map compartment_sets_; } // namespace detail -// CompartmentSetElement python API +// CompartmentLocation python API -CompartmentSetElement::CompartmentSetElement(const uint64_t gid, - const std::string& section_name, - const uint64_t section_index, - const double location) - : impl_(new detail::CompartmentSetElement(gid, section_name, section_index, location)) {} +CompartmentLocation::CompartmentLocation(const int64_t gid, + const int64_t section_idx, + const double offset) + : impl_(new detail::CompartmentLocation(gid, section_idx, offset)) {} -CompartmentSetElement::CompartmentSetElement(const std::string& content) - : impl_(new detail::CompartmentSetElement(content)) {} +CompartmentLocation::CompartmentLocation(const std::string& content) + : impl_(new detail::CompartmentLocation(content)) {} -CompartmentSetElement::CompartmentSetElement(std::unique_ptr&& impl) +CompartmentLocation::CompartmentLocation(std::unique_ptr&& impl) : impl_(std::move(impl)) {} -CompartmentSetElement::CompartmentSetElement(CompartmentSetElement&&) noexcept = default; -CompartmentSetElement& CompartmentSetElement::operator=(CompartmentSetElement&&) noexcept = default; -CompartmentSetElement::~CompartmentSetElement() = default; +CompartmentLocation::CompartmentLocation(CompartmentLocation&&) noexcept = default; +CompartmentLocation& CompartmentLocation::operator=(CompartmentLocation&&) noexcept = default; +CompartmentLocation::~CompartmentLocation() = default; -uint64_t CompartmentSetElement::gid() const { - return impl_->gid(); +bool CompartmentLocation::operator==(const CompartmentLocation& other) const noexcept { + return *impl_ == *(other.impl_); } -const std::string& CompartmentSetElement::sectionName() const { - return impl_->sectionName(); + +uint64_t CompartmentLocation::gid() const { + return impl_->gid(); } -uint64_t CompartmentSetElement::sectionIndex() const { - return impl_->sectionIndex(); + +uint64_t CompartmentLocation::sectionIdx() const { + return impl_->sectionIdx(); } -double CompartmentSetElement::location() const { - return impl_->location(); +double CompartmentLocation::offset() const { + return impl_->offset(); } -std::string CompartmentSetElement::toJSON() const { +std::string CompartmentLocation::toJSON() const { return impl_->to_json().dump(4); // Pretty print with 4 spaces } @@ -268,16 +317,17 @@ const std::string& CompartmentSet::population() const { return impl_->population(); } -std::vector CompartmentSet::getElements() { - std::vector view; - view.reserve(impl_->getElements().size()); - for (auto& el : impl_->getElements()) { - view.emplace_back(std::make_unique(el)); +std::vector CompartmentSet::getCompartmentLocations(const Selection& selection) const { + std::vector view; + auto raw_locs = impl_->getCompartmentLocations(selection); + view.reserve(raw_locs.size()); + for (auto& el : raw_locs) { + view.emplace_back(std::move(el)); // take ownership } return view; } -std::vector CompartmentSet::gids() const { +Selection CompartmentSet::gids() const { return impl_->gids(); } @@ -301,6 +351,14 @@ CompartmentSets CompartmentSets::fromFile(const std::string& path) { return CompartmentSets(detail::CompartmentSets::fromFile(path)); } +size_t CompartmentSets::size() const { + return impl_->size(); +} + +bool CompartmentSets::contains(const std::string& name) const { + return impl_->contains(name); +} + std::set CompartmentSets::names() const { return impl_->names(); } diff --git a/src/node_sets.cpp b/src/node_sets.cpp index 7a494f3..ada468d 100644 --- a/src/node_sets.cpp +++ b/src/node_sets.cpp @@ -462,14 +462,6 @@ class NodeSetCompoundRule: public NodeSetRule CompoundTargets targets_; }; -int64_t get_int64_or_throw(const json& el) { - auto v = el.get(); - if (std::floor(v) != v) { - throw SonataError(fmt::format("expected integer, got float {}", v)); - } - return static_cast(v); -} - NodeSetRulePtr _dispatch_node(const std::string& attribute, const json& value) { if (value.is_number()) { if (attribute == "population") { diff --git a/src/selection.cpp b/src/selection.cpp index 1c22b65..92056bb 100644 --- a/src/selection.cpp +++ b/src/selection.cpp @@ -119,6 +119,15 @@ Selection operator|(const Selection& lhs, const Selection& rhs) { return detail::union_(lhs.ranges(), rhs.ranges()); } +bool Selection::contains(Value gid) const { + auto it = std::lower_bound( + ranges_.begin(), ranges_.end(), gid, + [](const Range& range, Value v) { + return range[1] <= v; // Keep searching if gid >= end + }); + + return it != ranges_.end() && (*it)[0] <= gid && gid < (*it)[1]; +} } // namespace sonata } // namespace bbp diff --git a/src/utils.h b/src/utils.h index 02c29cd..b5feffe 100644 --- a/src/utils.h +++ b/src/utils.h @@ -48,6 +48,18 @@ std::set getMapKeys(const T& map) { } using json = nlohmann::json; + +inline int64_t get_int64_or_throw(const json& el) { + if (!el.is_number()) { + throw SonataError(fmt::format("expected integer, got {}", el.dump())); + } + auto v = el.get(); + if (std::floor(v) != v) { + throw SonataError(fmt::format("expected integer, got float {}", v)); + } + return static_cast(v); +} + inline uint64_t get_uint64_or_throw(const json& el) { if (!el.is_number()) { throw SonataError(fmt::format("expected unsigned integer, got {}", el.dump())); @@ -62,5 +74,6 @@ inline uint64_t get_uint64_or_throw(const json& el) { } return static_cast(v); } + } // namespace sonata } // namespace bbp diff --git a/test.py b/test.py index 13055f5..d9b0685 100644 --- a/test.py +++ b/test.py @@ -1,8 +1,9 @@ from libsonata import ( - CompartmentSetElement, + CompartmentLocation, CompartmentSets, CompartmentSet, SonataError, + Selection ) def inspect(v): @@ -11,5 +12,6 @@ def inspect(v): # if not i.startswith('__'): print(f' {i} = {getattr(v, i)}') -a = CompartmentSets('{ "CompartmentSet0": { "population": "pop0", "compartment_set": [[0, "dend", 10, 0.2], [2, "dend", 11, 0.2], [0, "dend", 11, 0.2], [1, "dend", 11, 0.2]] } }') -print(a.compartment_set("CompartmentSet0").gids()) \ No newline at end of file +a = CompartmentSets('{ "CompartmentSet0": { "population": "pop0", "compartment_set": [[0, 10, 0.2], [3, 11, 0.2], [0, 10, 0.201], [1, 11, 0.2]] } }') + +print("CompartmentSet" in a) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6f57bc8..e073549 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,12 +1,4 @@ -set(TESTS_SRC - main.cpp - test_config.cpp - test_edges.cpp - test_node_sets.cpp - test_nodes.cpp - test_report_reader.cpp - test_selection.cpp -) +file(GLOB TESTS_SRC "*.cpp") if(NOT EXTLIB_FROM_SUBMODULES) # When using submodules `include(.../Catch)` is performed diff --git a/tests/data/compartment_sets.json b/tests/data/compartment_sets.json index f7aa0ba..c9c3402 100644 --- a/tests/data/compartment_sets.json +++ b/tests/data/compartment_sets.json @@ -2,11 +2,11 @@ "example_compartment_set": { "population": "S1nonbarrel_neurons", "compartment_set": [ - [0, "dend", 10, 0.1], - [0, "dend", 10, 0.2], - [0, "dend", 10, 0.1], - [2, "axon", 3, 0.1], - [3, "dend", 6, 0.3] + [0, 10, 0.1], + [0, 10, 0.2], + [0, 10, 0.1], + [2, 3, 0.1], + [3, 6, 0.3] ] } } \ No newline at end of file diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp new file mode 100644 index 0000000..9c44714 --- /dev/null +++ b/tests/test_compartment_sets.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include + +using namespace bbp::sonata; + +TEST_CASE("CompartmentLocation: construction and JSON round-trip") { + CompartmentLocation loc(42, 3, 0.75); + REQUIRE(loc.gid() == 42); + REQUIRE(loc.sectionIdx() == 3); + REQUIRE(loc.offset() == Approx(0.75)); + + std::string json = loc.toJSON(); + CompartmentLocation parsed(json); + REQUIRE(parsed == loc); +} + +TEST_CASE("CompartmentSet: valid JSON parsing and access") { + std::string json = R"({ + "population": "exc", + "compartment_set": [ + [1, 0, 0.0], + [2, 1, 0.5], + [1, 2, 1.0] + ] + })"; + + CompartmentSet set(json); + REQUIRE(set.population() == "exc"); + + auto gids = set.gids(); + REQUIRE(gids.flatten().size() == 2); + REQUIRE(gids.contains(1)); + REQUIRE(gids.contains(2)); + + auto locs = set.getCompartmentLocations(); + REQUIRE(locs.size() == 3); + REQUIRE(locs[0].gid() == 1); + REQUIRE(locs[1].sectionIdx() == 1); + REQUIRE(locs[2].offset() == Approx(1.0)); +} + +TEST_CASE("CompartmentSets: parse multiple sets and query") { + std::string json = R"({ + "setA": { + "population": "pop1", + "compartment_set": [[10, 0, 0.0]] + }, + "setB": { + "population": "pop1", + "compartment_set": [[11, 1, 0.5], [12, 2, 0.7]] + } + })"; + + CompartmentSets sets(json); + REQUIRE(sets.size() == 2); + REQUIRE(sets.contains("setA")); + REQUIRE(sets.contains("setB")); + + auto names = sets.names(); + REQUIRE(names.size() == 2); + REQUIRE(names.find("setA") != names.end()); + REQUIRE(names.find("setB") != names.end()); + + auto setA = sets.getCompartmentSet("setA"); + REQUIRE(setA.gids().contains(10)); + + auto setB = sets.getCompartmentSet("setB"); + auto locs = setB.getCompartmentLocations(); + REQUIRE(locs.size() == 2); + REQUIRE(locs[1].offset() == Approx(0.7)); +} + +TEST_CASE("CompartmentSets: round-trip serialization") { + std::string json = R"({ + "cs0": { + "population": "P", + "compartment_set": [[1, 1, 0.1]] + } + })"; + + CompartmentSets sets(json); + std::string out = sets.toJSON(); + CompartmentSets reloaded(out); + + REQUIRE(reloaded.contains("cs0")); + auto set = reloaded.getCompartmentSet("cs0"); + auto locs = set.getCompartmentLocations(); + REQUIRE(locs.size() == 1); + REQUIRE(locs[0].gid() == 1); + REQUIRE(locs[0].offset() == Approx(0.1)); +} + +TEST_CASE("CompartmentSets: load from valid file") { + CompartmentSets sets = CompartmentSets::fromFile("./data/compartment_sets.json"); + REQUIRE(sets.size() > 0); + + for (const auto& name : sets.names()) { + auto set = sets.getCompartmentSet(name); + REQUIRE_FALSE(set.getCompartmentLocations().empty()); + } +} + + +TEST_CASE("CompartmentSet and CompartmentSets: throw SonataError on malformed JSON") { + SECTION("CompartmentSet: missing offset") { + std::string json = R"({ "population": "P", "compartment_set": [ [1, 1] ] })"; + CHECK_THROWS_AS(CompartmentSet(json), SonataError); + } + + SECTION("CompartmentSet: malformed array structure") { + std::string json = R"({ "population": "P", "compartment_set": [ 1, 2, 3 ] })"; + CHECK_THROWS_AS(CompartmentSet(json), SonataError); + } + + SECTION("CompartmentSet: missing population field") { + std::string json = R"({ "compartment_set": [ [1, 1, 0.5] ] })"; + CHECK_THROWS_AS(CompartmentSet(json), SonataError); + } + + SECTION("CompartmentSets: missing compartment_set in one entry") { + std::string json = R"({ "set1": { "population": "P" } })"; + CHECK_THROWS_AS(CompartmentSets(json), SonataError); + } + + SECTION("CompartmentSets: invalid inner structure") { + std::string json = R"({ "set1": [1, 2, 3] })"; + CHECK_THROWS_AS(CompartmentSets(json), SonataError); + } + + SECTION("CompartmentSets: malformed JSON string") { + std::string json = R"({ "set1": { "population": "P", "compartment_set": [ [1, 1 ] })"; // unclosed bracket + CHECK_THROWS_AS(CompartmentSets(json), std::exception); + } +} From a68a388798e8a216605d15db6ad1b27724a003b0 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Fri, 30 May 2025 17:08:28 +0200 Subject: [PATCH 10/44] formatted, WIP --- include/bbp/sonata/compartment_sets.h | 9 ++-- python/bindings.cpp | 61 +++++++++++++++------------ src/compartment_sets.cpp | 48 ++++++++++++--------- src/selection.cpp | 5 +-- 4 files changed, 69 insertions(+), 54 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index ee76f28..b4afe05 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -5,7 +5,7 @@ namespace bbp { namespace sonata { namespace detail { - class CompartmentLocation; +class CompartmentLocation; class CompartmentSet; class CompartmentSets; } // namespace detail @@ -23,7 +23,7 @@ class SONATA_API CompartmentLocation * \throw if content cannot be parsed */ explicit CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset); - explicit CompartmentLocation(const std::string& content); + explicit CompartmentLocation(const std::string& content); explicit CompartmentLocation(std::unique_ptr&& impl); CompartmentLocation(CompartmentLocation&&) noexcept; CompartmentLocation(const CompartmentLocation& other) = delete; @@ -37,7 +37,7 @@ class SONATA_API CompartmentLocation uint64_t gid() const; /** - * Absolute section index. Progressive index that uniquely identifies the section. + * Absolute section index. Progressive index that uniquely identifies the section. * There is a mapping between neuron section names (i.e. dend[10]) and this index. */ uint64_t sectionIdx() const; @@ -84,7 +84,8 @@ class SONATA_API CompartmentSet /** * Get the CompartmentLocations. */ - std::vector getCompartmentLocations(const Selection& selection = Selection({})) const; + std::vector getCompartmentLocations( + const Selection& selection = Selection({})) const; /** * Return the gids of the compartment locations. diff --git a/python/bindings.cpp b/python/bindings.cpp index c4dcd58..0b8e029 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -470,9 +470,10 @@ PYBIND11_MODULE(_libsonata, m) { .def( "flatten", [](Selection& obj) { return asArray(obj.flatten()); }, DOC_SEL(flatten)) .def_property_readonly("flat_size", &Selection::flatSize, DOC_SEL(flatSize)) - .def("__contains__", [](const Selection& sel, uint64_t gid) { - return sel.contains(gid); - }, "Check if a GID is contained in the selection") + .def( + "__contains__", + [](const Selection& sel, uint64_t gid) { return sel.contains(gid); }, + "Check if a GID is contained in the selection") .def( "__bool__", [](const Selection& obj) { return !obj.empty(); }, @@ -546,35 +547,40 @@ PYBIND11_MODULE(_libsonata, m) { .def(py::init()) .def(py::init()) .def_property_readonly("gid", &CompartmentLocation::gid, DOC_COMPARTMENTLOCATION(gid)) - .def_property_readonly("section_idx", &CompartmentLocation::sectionIdx, DOC_COMPARTMENTLOCATION(sectionIdx)) - .def_property_readonly("offset", &CompartmentLocation::offset, DOC_COMPARTMENTLOCATION(offset)) - .def("__iter__", [](const CompartmentLocation& self) { - return py::iter(py::make_tuple( - self.gid(), - self.sectionIdx(), - self.offset() - )); - }) - .def("__repr__", [](const CompartmentLocation& self) { - return py::str("CompartmentLocation({}, {}, {})") - .format(self.gid(), self.sectionIdx(), self.offset()); - }) - .def("__str__", [](const CompartmentLocation& self) { - return py::str(py::repr(py::cast(self))); // Delegates to __repr__ - }) + .def_property_readonly("section_idx", + &CompartmentLocation::sectionIdx, + DOC_COMPARTMENTLOCATION(sectionIdx)) + .def_property_readonly("offset", + &CompartmentLocation::offset, + DOC_COMPARTMENTLOCATION(offset)) + .def("__iter__", + [](const CompartmentLocation& self) { + return py::iter(py::make_tuple(self.gid(), self.sectionIdx(), self.offset())); + }) + .def("__repr__", + [](const CompartmentLocation& self) { + return py::str("CompartmentLocation({}, {}, {})") + .format(self.gid(), self.sectionIdx(), self.offset()); + }) + .def("__str__", + [](const CompartmentLocation& self) { + return py::str(py::repr(py::cast(self))); // Delegates to __repr__ + }) .def("__eq__", &CompartmentLocation::operator==) .def("toJSON", &CompartmentLocation::toJSON, DOC_COMPARTMENTLOCATION(toJSON)); py::class_(m, "CompartmentSet", "CompartmentSet") .def(py::init()) - .def_property_readonly("population", &CompartmentSet::population, DOC_COMPARTMENTSET(population)) - .def("compartment_locations", + .def_property_readonly("population", + &CompartmentSet::population, + DOC_COMPARTMENTSET(population)) + .def( + "compartment_locations", [](const CompartmentSet& self, const Selection& sel) { return self.getCompartmentLocations(sel); - }, + }, py::arg("selection") = Selection({}), // provide default arg here - DOC_COMPARTMENTSET(getCompartmentLocations) - ) + DOC_COMPARTMENTSET(getCompartmentLocations)) .def("gids", &CompartmentSet::gids, DOC_COMPARTMENTSET(gids)) .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)); @@ -582,8 +588,11 @@ PYBIND11_MODULE(_libsonata, m) { .def(py::init()) .def("__contains__", &CompartmentSets::contains) .def("__len__", &CompartmentSets::size) - .def("compartment_set", &CompartmentSets::getCompartmentSet, DOC_COMPARTMENTSETS(getCompartmentSet)) - .def_static("from_file", [](py::object path) { return CompartmentSets::fromFile(py::str(path)); }) + .def("compartment_set", + &CompartmentSets::getCompartmentSet, + DOC_COMPARTMENTSETS(getCompartmentSet)) + .def_static("from_file", + [](py::object path) { return CompartmentSets::fromFile(py::str(path)); }) .def_property_readonly("names", &CompartmentSets::names, DOC_COMPARTMENTSETS(names)) .def("toJSON", &CompartmentSets::toJSON, DOC_COMPARTMENTSETS(toJSON)); diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 5ae2ec0..7b3ff44 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -1,8 +1,8 @@ #include "../extlib/filesystem.hpp" -#include -#include #include +#include +#include #include "utils.h" // readFile @@ -16,7 +16,8 @@ namespace detail { using json = nlohmann::json; -class CompartmentLocation { +class CompartmentLocation +{ std::uint64_t gid_; std::uint64_t section_idx_; double offset_; @@ -29,18 +30,20 @@ class CompartmentLocation { } void setSectionIdx(int64_t section_idx) { if (section_idx < 0) { - throw SonataError(fmt::format("Section index must be non-negative, got {}", section_idx)); + throw SonataError( + fmt::format("Section index must be non-negative, got {}", section_idx)); } section_idx_ = static_cast(section_idx); } void setOffset(double offset) { if (offset < 0.0 || offset > 1.0) { - throw SonataError(fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); + throw SonataError( + fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); } offset_ = offset; } -public: + public: static constexpr double offsetTolerance = 1e-4; static constexpr double offsetToleranceInv = 1.0 / offsetTolerance; @@ -51,11 +54,13 @@ class CompartmentLocation { } explicit CompartmentLocation(const std::string& content) - : CompartmentLocation(json::parse(content)) {} + : CompartmentLocation(json::parse(content)) { } CompartmentLocation(const nlohmann::json& j) { if (!j.is_array() || j.size() != 3) { - throw SonataError("CompartmentLocation must be an array of exactly 3 elements: [gid, section_idx, offset]"); + throw SonataError( + "CompartmentLocation must be an array of exactly 3 elements: [gid, section_idx, " + "offset]"); } setGid(get_uint64_or_throw(j[0])); @@ -84,9 +89,8 @@ class CompartmentLocation { } bool operator==(const CompartmentLocation& other) const { - return gid_ == other.gid_ - && section_idx_ == other.section_idx_ - && std::abs(offset_ - other.offset_) < offsetTolerance; + return gid_ == other.gid_ && section_idx_ == other.section_idx_ && + std::abs(offset_ - other.offset_) < offsetTolerance; } }; @@ -98,7 +102,8 @@ struct CompartmentLocationHash { // Quantize offset to 4 decimal places double offset = loc.offset(); - uint64_t quantized_offset = static_cast(std::round(offset * CompartmentLocation::offsetToleranceInv)); + uint64_t quantized_offset = static_cast( + std::round(offset * CompartmentLocation::offsetToleranceInv)); std::size_t h3 = std::hash{}(quantized_offset); @@ -116,7 +121,7 @@ class CompartmentSet { std::string population_; std::vector compartment_locations_; -public: + public: explicit CompartmentSet(const std::string& content) : CompartmentSet(json::parse(content)) {} @@ -166,8 +171,8 @@ class CompartmentSet { return population_; } - std::vector> - getCompartmentLocations(const Selection& selection) const { + std::vector> getCompartmentLocations( + const Selection& selection) const { std::vector> result; result.reserve(compartment_locations_.size()); @@ -267,15 +272,15 @@ std::map compartment_sets_; // CompartmentLocation python API CompartmentLocation::CompartmentLocation(const int64_t gid, - const int64_t section_idx, - const double offset) - : impl_(new detail::CompartmentLocation(gid, section_idx, offset)) {} + const int64_t section_idx, + const double offset) + : impl_(new detail::CompartmentLocation(gid, section_idx, offset)) { } CompartmentLocation::CompartmentLocation(const std::string& content) - : impl_(new detail::CompartmentLocation(content)) {} + : impl_(new detail::CompartmentLocation(content)) { } CompartmentLocation::CompartmentLocation(std::unique_ptr&& impl) - : impl_(std::move(impl)) {} + : impl_(std::move(impl)) { } CompartmentLocation::CompartmentLocation(CompartmentLocation&&) noexcept = default; CompartmentLocation& CompartmentLocation::operator=(CompartmentLocation&&) noexcept = default; @@ -317,7 +322,8 @@ const std::string& CompartmentSet::population() const { return impl_->population(); } -std::vector CompartmentSet::getCompartmentLocations(const Selection& selection) const { +std::vector CompartmentSet::getCompartmentLocations( + const Selection& selection) const { std::vector view; auto raw_locs = impl_->getCompartmentLocations(selection); view.reserve(raw_locs.size()); diff --git a/src/selection.cpp b/src/selection.cpp index 92056bb..0515c48 100644 --- a/src/selection.cpp +++ b/src/selection.cpp @@ -120,9 +120,8 @@ Selection operator|(const Selection& lhs, const Selection& rhs) { } bool Selection::contains(Value gid) const { - auto it = std::lower_bound( - ranges_.begin(), ranges_.end(), gid, - [](const Range& range, Value v) { + auto it = + std::lower_bound(ranges_.begin(), ranges_.end(), gid, [](const Range& range, Value v) { return range[1] <= v; // Keep searching if gid >= end }); From bf2b6d94391886a028936fa178c3a209bf0e6a1f Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Fri, 30 May 2025 17:38:41 +0200 Subject: [PATCH 11/44] fix clang-format9? --- src/compartment_sets.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 7b3ff44..62a79e2 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -54,7 +54,7 @@ class CompartmentLocation } explicit CompartmentLocation(const std::string& content) - : CompartmentLocation(json::parse(content)) { } + : CompartmentLocation(json::parse(content)) {} CompartmentLocation(const nlohmann::json& j) { if (!j.is_array() || j.size() != 3) { @@ -122,7 +122,6 @@ class CompartmentSet { std::vector compartment_locations_; public: - explicit CompartmentSet(const std::string& content) : CompartmentSet(json::parse(content)) {} @@ -274,13 +273,13 @@ std::map compartment_sets_; CompartmentLocation::CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset) - : impl_(new detail::CompartmentLocation(gid, section_idx, offset)) { } + : impl_(new detail::CompartmentLocation(gid, section_idx, offset)) {} CompartmentLocation::CompartmentLocation(const std::string& content) - : impl_(new detail::CompartmentLocation(content)) { } + : impl_(new detail::CompartmentLocation(content)) {} CompartmentLocation::CompartmentLocation(std::unique_ptr&& impl) - : impl_(std::move(impl)) { } + : impl_(std::move(impl)) {} CompartmentLocation::CompartmentLocation(CompartmentLocation&&) noexcept = default; CompartmentLocation& CompartmentLocation::operator=(CompartmentLocation&&) noexcept = default; From bb7ca75953c274aabd8a39689327acfdfe8465ce Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Tue, 3 Jun 2025 13:39:20 +0200 Subject: [PATCH 12/44] CompartmentLocation --- include/bbp/sonata/compartment_sets.h | 208 +++++---- include/bbp/sonata/selection.h | 4 + python/bindings.cpp | 96 ++-- python/generated/docstrings.h | 14 +- python/libsonata/__init__.py | 4 - python/tests/test_compartmentsets.py | 309 ++++++++----- src/compartment_sets.cpp | 602 +++++++++++++++----------- src/selection.cpp | 1 - test.py | 13 +- tests/data/compartment_sets.json | 8 +- tests/test_compartment_sets.cpp | 307 ++++++++----- 11 files changed, 918 insertions(+), 648 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index b4afe05..2ebc7b4 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -10,46 +10,37 @@ class CompartmentSet; class CompartmentSets; } // namespace detail +/** + * CompartmentLocation public API. + * + * This class uniquely identifies a compartment by a set of gid, section_idx and offset: + * + * - gid: Global ID of the cell (Neuron) to which the compartment belongs. No + * overlaps among populations. + * - section_idx: Absolute section index. Progressive index that uniquely identifies the section. + * There is a mapping between neuron section names (i.e. dend[10]) and this index. + * - offset: Offset of the compartment along the section. The offset is a value between 0 and 1 + */ class SONATA_API CompartmentLocation { public: - /** - * Create CompartmentLocation from JSON - * - * See also: - * TODO - * - * \param content is the JSON compartment_set value - * \throw if content cannot be parsed - */ - explicit CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset); + CompartmentLocation() = delete; + CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset); explicit CompartmentLocation(const std::string& content); explicit CompartmentLocation(std::unique_ptr&& impl); + CompartmentLocation(const CompartmentLocation& other); + CompartmentLocation& operator=(const CompartmentLocation& other); CompartmentLocation(CompartmentLocation&&) noexcept; - CompartmentLocation(const CompartmentLocation& other) = delete; CompartmentLocation& operator=(CompartmentLocation&&) noexcept; - bool operator==(const CompartmentLocation& other) const noexcept; ~CompartmentLocation(); - /** - * GID - */ - uint64_t gid() const; + bool operator==(const CompartmentLocation& other) const noexcept; + bool operator!=(const CompartmentLocation& other) const noexcept; - /** - * Absolute section index. Progressive index that uniquely identifies the section. - * There is a mapping between neuron section names (i.e. dend[10]) and this index. - */ + uint64_t gid() const; uint64_t sectionIdx() const; - - /** - * Offset of the compartment along the section. - */ double offset() const; - /** - * Return the nodesets as a JSON string. - */ std::string toJSON() const; private: @@ -57,90 +48,85 @@ class SONATA_API CompartmentLocation }; -class SONATA_API CompartmentSet -{ - public: - /** - * Create CompartmentSet from JSON - * - * See also: - * TODO - * - * \param content is the JSON compartment_set value - * \throw if content cannot be parsed - */ - explicit CompartmentSet(const std::string& content); - explicit CompartmentSet(std::unique_ptr&& impl); - CompartmentSet(CompartmentSet&&) noexcept; - CompartmentSet(const CompartmentSet& other) = delete; - CompartmentSet& operator=(CompartmentSet&&) noexcept; - ~CompartmentSet(); - - /** - * Population name - */ - const std::string& population() const; - - /** - * Get the CompartmentLocations. - */ - std::vector getCompartmentLocations( - const Selection& selection = Selection({})) const; - - /** - * Return the gids of the compartment locations. - */ - Selection gids() const; - - /** - * Return the nodesets as a JSON string. - */ - std::string toJSON() const; - - private: - std::unique_ptr impl_; -}; - -class SONATA_API CompartmentSets -{ - public: - /** - * Create CompartmentSets from JSON - * - * See also: - * TODO - * - * \param content is the JSON compartment_sets value - * \throw if content cannot be parsed - */ - explicit CompartmentSets(const std::string& content); - explicit CompartmentSets(std::unique_ptr&& impl); - CompartmentSets(CompartmentSets&&) noexcept; - CompartmentSets(const CompartmentSets& other) = delete; - CompartmentSets& operator=(CompartmentSets&&) noexcept; - ~CompartmentSets(); - size_t size() const; - bool contains(const std::string& name) const; - - /** Open a SONATA `Compartment sets` file from a path */ - static CompartmentSets fromFile(const std::string& path); - - /** - * Names of the node sets available - */ - std::set names() const; - - /** - * Return the nodesets as a JSON string. - */ - std::string toJSON() const; - - // TODO - CompartmentSet getCompartmentSet(const std::string& name); - - private: - std::unique_ptr impl_; -}; +// class SONATA_API CompartmentSet +// { +// public: +// /** +// * Create CompartmentSet from JSON +// * +// * See also: +// * TODO +// * +// * \param content is the JSON compartment_set value +// * \throw if content cannot be parsed +// */ +// CompartmentSet(const std::string& content); +// CompartmentSet(std::unique_ptr&& impl); +// CompartmentSet(detail::CompartmentSet&& impl); +// CompartmentSet(CompartmentSet&&) noexcept; +// CompartmentSet(const CompartmentSet& other) = delete; +// CompartmentSet& operator=(CompartmentSet&&) noexcept; +// ~CompartmentSet(); + + +// std::size_t size() const; + +// CompartmentLocation operator[](std::size_t index) const; + +// /** +// * Population name +// */ +// const std::string& population() const; + +// /** +// * Get the CompartmentLocations. +// */ +// std::vector locations( +// const Selection& selection = Selection({})) const; + +// /** +// * Return the gids of the compartment locations. +// */ +// Selection gids() const; + +// /** +// * Return the nodesets as a JSON string. +// */ +// std::string toJSON() const; + +// private: +// std::unique_ptr impl_; +// }; + +// class SONATA_API CompartmentSets +// { +// public: +// // Keep these exactly as-is: +// CompartmentSets(const std::string& content); +// CompartmentSets(std::unique_ptr&& impl); +// CompartmentSets(detail::CompartmentSets&& impl); +// CompartmentSets(CompartmentSets&&) noexcept; +// CompartmentSets(const CompartmentSets& other) = delete; +// CompartmentSets& operator=(CompartmentSets&&) noexcept; +// ~CompartmentSets(); + +// static CompartmentSets fromFile(const std::string& path); + +// // Read-only dict-like API, Python-style names: +// size_t size() const; +// bool contains(const std::string& name) const; + +// std::vector keys() const; +// std::vector values() const; +// std::vector> items() const; + +// CompartmentSet get(const std::string& name) const; + +// std::string toJSON() const; + +// private: +// std::unique_ptr impl_; +// }; } // namespace sonata } // namespace bbp diff --git a/include/bbp/sonata/selection.h b/include/bbp/sonata/selection.h index cc5de9b..5886a9f 100644 --- a/include/bbp/sonata/selection.h +++ b/include/bbp/sonata/selection.h @@ -17,6 +17,10 @@ class SONATA_API Selection using Range = std::array; using Ranges = std::vector; + /** + * Create Selection from a list of ranges + * @param ranges is a list of ranges constituting Selection + */ Selection(Ranges ranges); template diff --git a/python/bindings.cpp b/python/bindings.cpp index 0b8e029..8671ee4 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -542,10 +542,11 @@ PYBIND11_MODULE(_libsonata, m) { .def("materialize", &NodeSets::materialize, DOC_NODESETS(materialize)) .def("update", &NodeSets::update, "other"_a, DOC_NODESETS(update)) .def("toJSON", &NodeSets::toJSON, DOC_NODESETS(toJSON)); - - py::class_(m, "CompartmentLocation", "CompartmentLocation") + + py::class_(m, "CompartmentLocation") .def(py::init()) - .def(py::init()) + .def(py::init(), + py::arg("gid"), py::arg("section_idx"), py::arg("offset")) .def_property_readonly("gid", &CompartmentLocation::gid, DOC_COMPARTMENTLOCATION(gid)) .def_property_readonly("section_idx", &CompartmentLocation::sectionIdx, @@ -553,10 +554,20 @@ PYBIND11_MODULE(_libsonata, m) { .def_property_readonly("offset", &CompartmentLocation::offset, DOC_COMPARTMENTLOCATION(offset)) + .def("toJSON", &CompartmentLocation::toJSON, DOC_COMPARTMENTLOCATION(toJSON)) .def("__iter__", [](const CompartmentLocation& self) { return py::iter(py::make_tuple(self.gid(), self.sectionIdx(), self.offset())); }) + .def("__eq__", &CompartmentLocation::operator==) + .def("__ne__", &CompartmentLocation::operator!=) + .def("__copy__", [](const CompartmentLocation& self) { + return CompartmentLocation(self); + }) + .def("__deepcopy__", + [](const CompartmentLocation& self, py::dict /* memo */) { + return CompartmentLocation(self); + }) .def("__repr__", [](const CompartmentLocation& self) { return py::str("CompartmentLocation({}, {}, {})") @@ -564,37 +575,54 @@ PYBIND11_MODULE(_libsonata, m) { }) .def("__str__", [](const CompartmentLocation& self) { - return py::str(py::repr(py::cast(self))); // Delegates to __repr__ - }) - .def("__eq__", &CompartmentLocation::operator==) - .def("toJSON", &CompartmentLocation::toJSON, DOC_COMPARTMENTLOCATION(toJSON)); - - py::class_(m, "CompartmentSet", "CompartmentSet") - .def(py::init()) - .def_property_readonly("population", - &CompartmentSet::population, - DOC_COMPARTMENTSET(population)) - .def( - "compartment_locations", - [](const CompartmentSet& self, const Selection& sel) { - return self.getCompartmentLocations(sel); - }, - py::arg("selection") = Selection({}), // provide default arg here - DOC_COMPARTMENTSET(getCompartmentLocations)) - .def("gids", &CompartmentSet::gids, DOC_COMPARTMENTSET(gids)) - .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)); - - py::class_(m, "CompartmentSets", "CompartmentSets") - .def(py::init()) - .def("__contains__", &CompartmentSets::contains) - .def("__len__", &CompartmentSets::size) - .def("compartment_set", - &CompartmentSets::getCompartmentSet, - DOC_COMPARTMENTSETS(getCompartmentSet)) - .def_static("from_file", - [](py::object path) { return CompartmentSets::fromFile(py::str(path)); }) - .def_property_readonly("names", &CompartmentSets::names, DOC_COMPARTMENTSETS(names)) - .def("toJSON", &CompartmentSets::toJSON, DOC_COMPARTMENTSETS(toJSON)); + return py::str(py::repr(py::cast(self))); + }); + + + + // py::class_(m, "CompartmentSet", "CompartmentSet") + // .def(py::init()) + // .def_property_readonly("population", + // &CompartmentSet::population, + // DOC_COMPARTMENTSET(population)) + // .def("__len__", [](const CompartmentSet& self) { + // return self.size(); + // }) + // .def("__getitem__", [](const CompartmentSet& self, py::ssize_t i) { + // if (i < 0) { + // i += static_cast(self.size()); + // } + // if (i < 0 || static_cast(i) >= self.size()) { + // throw py::index_error("Index out of range"); + // } + // return self[static_cast(i)]; + // }, py::arg("index")) + // .def("gids", &CompartmentSet::gids) + // .def( + // "locations", + // [](const CompartmentSet& self, const Selection& sel = Selection({})) { + // return self.locations(sel); + // }, + // py::arg("selection") = Selection({}), + // DOC_COMPARTMENTSET(locations)) + // .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)); + + // py::class_(m, "CompartmentSets", "CompartmentSets") + // .def(py::init()) + // .def_static( + // "from_file", + // [](py::object path) { + // return CompartmentSets::fromFile(py::str(path)); + // }, + // py::arg("path"), + // "Create a CompartmentSets object from a file") + // .def("__contains__", &CompartmentSets::contains) + // .def("__len__", &CompartmentSets::size) + // .def("__getitem__", &CompartmentSets::get, py::arg("name")) + // .def("keys", &CompartmentSets::keys, DOC_COMPARTMENTSETS(keys)) + // .def("values", &CompartmentSets::values, DOC_COMPARTMENTSETS(values)) + // .def("items", &CompartmentSets::items, DOC_COMPARTMENTSETS(items)) + // .def("toJSON", &CompartmentSets::toJSON, DOC_COMPARTMENTSETS(toJSON)); py::class_(m, "CommonPopulationProperties", diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 7e967b4..8a89f21 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -389,19 +389,21 @@ static const char *__doc_bbp_sonata_CompartmentLocation_offset = R"doc(Offset of static const char *__doc_bbp_sonata_CompartmentLocation_toJSON = R"doc(Return the compartment set element as a JSON string.)doc"; -static const char *__doc_bbp_sonata_CompartmentSet_getCompartmentLocations = R"doc(Get the list of CompartmentLocations.)doc"; +static const char *__doc_bbp_sonata_CompartmentSet_population = R"doc(Population name)doc"; + +static const char *__doc_bbp_sonata_CompartmentSet_gids = R"doc(Gids in the list of CompartmentLocations.)doc"; -static const char *__doc_bbp_sonata_CompartmentSet_gids = R"doc(Gids in the list of CompartmentLocation.)doc"; +static const char *__doc_bbp_sonata_CompartmentSet_locations = R"doc(Get the list of CompartmentLocations.)doc"; static const char *__doc_bbp_sonata_CompartmentSet_toJSON = R"doc(Return the compartment set as a JSON string.)doc"; -static const char *__doc_bbp_sonata_CompartmentSet_population = R"doc(Population name)doc"; +static const char *__doc_bbp_sonata_CompartmentSets_keys = R"doc(Return the keys of the CompartmentSets)doc"; -static const char *__doc_bbp_sonata_CompartmentSets_toJSON = R"doc(Return the compartment sets as a JSON string.)doc"; +static const char *__doc_bbp_sonata_CompartmentSets_values = R"doc(Return the values of the CompartmentSets)doc"; -static const char *__doc_bbp_sonata_CompartmentSets_names = R"doc(Names of the compartment sets available)doc"; +static const char *__doc_bbp_sonata_CompartmentSets_items = R"doc(Return the (key, value) pairs of the CompartmentSets)doc"; -static const char *__doc_bbp_sonata_CompartmentSets_getCompartmentSet = R"doc(Get compartment set)doc"; +static const char *__doc_bbp_sonata_CompartmentSets_toJSON = R"doc(Serialize CompartmentSets to a JSON string)doc"; static const char *__doc_bbp_sonata_NodeSets_update = R"doc(Update `this` to include all nodesets from `this` and `other`. diff --git a/python/libsonata/__init__.py b/python/libsonata/__init__.py index 93fce0a..6b441a7 100644 --- a/python/libsonata/__init__.py +++ b/python/libsonata/__init__.py @@ -15,8 +15,6 @@ NodePopulation, NodeSets, CompartmentLocation, - CompartmentSet, - CompartmentSets, NodeStorage, Selection, SomaDataFrame, @@ -42,8 +40,6 @@ "NodePopulation", "NodeSets", "CompartmentLocation", - "CompartmentSet", - "CompartmentSets", "NodeStorage", "Selection", "SomaDataFrame", diff --git a/python/tests/test_compartmentsets.py b/python/tests/test_compartmentsets.py index dcb7352..af081d5 100644 --- a/python/tests/test_compartmentsets.py +++ b/python/tests/test_compartmentsets.py @@ -3,116 +3,207 @@ import unittest from libsonata import ( - CompartmentSets, - SonataError, + CompartmentLocation, +# CompartmentSet, +# CompartmentSets, +# SonataError, ) PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../tests/data') -class TestCompartmentSetsFailure(unittest.TestCase): - def test_CorrectStructure(self): - # Top level must be an object - self.assertRaises(SonataError, CompartmentSets, "1") - self.assertRaises(SonataError, CompartmentSets, '["array"]') - - # Each CompartmentSet must be an object with population and compartment_set keys - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": 1 }') - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": "string" }') - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": null }') - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": true }') - - def test_MissingPopulationOrCompartmentSet(self): - # Missing population key - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "compartment_set": [] } }') - # Missing compartment_set key - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop" } }') - - def test_InvalidPopulationType(self): - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": 123, "compartment_set": [] } }') - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": null, "compartment_set": [] } }') - - def test_InvalidCompartmentSetType(self): - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": "not an array" } }') - self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": 123 } }') - - def test_InvalidCompartmentLocationStructure(self): - # Each compartment_set element must be an array of 4 elements [gid, section_name, section_index, location] - - # Not an array - self.assertRaises(SonataError, CompartmentSets, ''' - { - "CompartmentSet0": { - "population": "pop", - "compartment_set": [ 1 ] - } - } - ''') - - # Array with wrong size - self.assertRaises(SonataError, CompartmentSets, ''' - { - "CompartmentSet0": { - "population": "pop", - "compartment_set": [ [1, 0] ] - } - } - ''') - - # Wrong types inside element - self.assertRaises(SonataError, CompartmentSets, ''' - { - "CompartmentSet0": { - "population": "pop", - "compartment_set": [ ["not uint64", 0, 0.5] ] - } - } - ''') - - - self.assertRaises(SonataError, CompartmentSets, ''' - { - "CompartmentSet0": { - "population": "pop", - "compartment_set": [ [1, "not uint64", 0.5] ] - } - } - ''') - - self.assertRaises(SonataError, CompartmentSets, ''' - { - "CompartmentSet0": { - "population": "pop", - "compartment_set": [ [1, 0, "not a number"] ] - } - } - ''') - - # Location out of bounds - self.assertRaises(SonataError, CompartmentSets, ''' - { - "CompartmentSet0": { - "population": "pop", - "compartment_set": [ [1, 0, -0.1] ] - } - } - ''') - - self.assertRaises(SonataError, CompartmentSets, ''' - { - "CompartmentSet0": { - "population": "pop", - "compartment_set": [ [1, 0, 1.1] ] - } - } - ''') - - def test_MissingFile(self): - self.assertRaises(SonataError, CompartmentSets.from_file, 'this/file/does/not/exist') - - -# class TestCompartmentSet(unittest.TestCase): -# def setUp(self): -# self.compartment_sets = CompartmentSets.from_file(os.path.join(PATH, "compartment_sets.json")) - -# def test_BasicCompartmentIdSelection(self): -# pass + +class TestCompartmentLocation(unittest.TestCase): + def test_constructor_from_values(self): + loc = CompartmentLocation(4, 40, 0.9) + self.assertEqual(loc.gid, 4) + self.assertEqual(loc.section_idx, 40) + self.assertAlmostEqual(loc.offset, 0.9) + + def test_constructor_from_string(self): + loc = CompartmentLocation("[4, 40, 0.9]") + self.assertEqual(loc.gid, 4) + self.assertEqual(loc.section_idx, 40) + self.assertAlmostEqual(loc.offset, 0.9) + + def test_toJSON(self): + loc = CompartmentLocation(4, 40, 0.9) + self.assertEqual(loc.toJSON(), "[4,40,0.9]") + + def test_equality(self): + loc1 = CompartmentLocation(4, 40, 0.9) + loc2 = CompartmentLocation("[4, 40, 0.9]") + loc3 = CompartmentLocation(5, 40, 0.9) + self.assertEqual(loc1, loc2) + self.assertNotEqual(loc1, loc3) + + def test_repr_and_str(self): + loc = CompartmentLocation(4, 40, 0.9) + expected = "CompartmentLocation(4, 40, 0.9)" + self.assertEqual(repr(loc), expected) + self.assertEqual(str(loc), repr(loc)) # str should delegate to repr + + def test_iterable(self): + loc = CompartmentLocation(4, 40, 0.9) + gid, section_idx, offset = loc + self.assertEqual(gid, 4) + self.assertEqual(section_idx, 40) + self.assertAlmostEqual(offset, 0.9) + + def test_assignment_creates_copy(self): + loc1 = CompartmentLocation(1, 2, 0.3) + loc2 = loc1 # This is a reference assignment in Python + self.assertEqual(loc1, loc2) + + # Now mutate loc1 and check if loc2 is affected — which it will be, unless toJSON etc. are implemented with deep semantics + self.assertIs(loc1, loc2) # They reference the same object + + def test_explicit_copy(self): + import copy + loc1 = CompartmentLocation(1, 2, 0.3) + loc2 = copy.copy(loc1) + self.assertEqual(loc1, loc2) + self.assertIsNot(loc1, loc2) # Ensure they’re distinct objects + + def test_deepcopy(self): + import copy + loc1 = CompartmentLocation(1, 2, 0.3) + loc2 = copy.deepcopy(loc1) + self.assertEqual(loc1, loc2) + self.assertIsNot(loc1, loc2) + + +# class TestCompartmentSetsFailure(unittest.TestCase): +# def test_CorrectStructure(self): +# # Top level must be an object +# self.assertRaises(SonataError, CompartmentSets, "1") +# self.assertRaises(SonataError, CompartmentSets, '["array"]') + +# # Each CompartmentSet must be an object with population and compartment_set keys +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": 1 }') +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": "string" }') +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": null }') +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": true }') + +# def test_MissingPopulationOrCompartmentSet(self): +# # Missing population key +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "compartment_set": [] } }') +# # Missing compartment_set key +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop" } }') + +# def test_InvalidPopulationType(self): +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": 123, "compartment_set": [] } }') +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": null, "compartment_set": [] } }') + +# def test_InvalidCompartmentSetType(self): +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": "not an array" } }') +# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": 123 } }') + +# def test_InvalidCompartmentLocationStructure(self): +# # Each compartment_set element must be an array of 3 elements [gid, section_idx, location] + +# # Not an array +# self.assertRaises(SonataError, CompartmentSets, ''' +# { +# "CompartmentSet0": { +# "population": "pop", +# "compartment_set": [ 1 ] +# } +# } +# ''') + +# # Array with wrong size +# self.assertRaises(SonataError, CompartmentSets, ''' +# { +# "CompartmentSet0": { +# "population": "pop", +# "compartment_set": [ [1, 0] ] +# } +# } +# ''') + +# # Wrong types inside element +# self.assertRaises(SonataError, CompartmentSets, ''' +# { +# "CompartmentSet0": { +# "population": "pop", +# "compartment_set": [ ["not uint64", 0, 0.5] ] +# } +# } +# ''') + + +# self.assertRaises(SonataError, CompartmentSets, ''' +# { +# "CompartmentSet0": { +# "population": "pop", +# "compartment_set": [ [1, "not uint64", 0.5] ] +# } +# } +# ''') + +# self.assertRaises(SonataError, CompartmentSets, ''' +# { +# "CompartmentSet0": { +# "population": "pop", +# "compartment_set": [ [1, 0, "not a number"] ] +# } +# } +# ''') + +# # Location out of bounds +# self.assertRaises(SonataError, CompartmentSets, ''' +# { +# "CompartmentSet0": { +# "population": "pop", +# "compartment_set": [ [1, 0, -0.1] ] +# } +# } +# ''') + +# self.assertRaises(SonataError, CompartmentSets, ''' +# { +# "CompartmentSet0": { +# "population": "pop", +# "compartment_set": [ [1, 0, 1.1] ] +# } +# } +# ''') + +# def test_MissingFile(self): +# self.assertRaises(SonataError, CompartmentSets.from_file, 'this/file/does/not/exist') + + +# class TestLoadValidCompartmentSets(unittest.TestCase): +# def test_valid_compartment_sets_file(self): +# # Load a valid compartment sets file +# file_path = os.path.join(PATH, 'compartment_sets.json') +# cs = CompartmentSets.from_file(file_path) + +# self.assertIsInstance(cs, CompartmentSets) +# self.assertEqual(len(cs), 2) +# self.assertEqual(['cs0', 'cs1'], cs.keys()) +# self.assertIn('cs0', cs) +# self.assertFalse('cs2' in cs) +# self.assertIsInstance(cs['cs0'], CompartmentSet) +# self.assertEqual(cs['cs0'].population, 'pop0') +# self.assertEqual(len(cs.values()), 2) +# self.assertEqual(len(cs.items()), 2) + +# class TestCompartmentLocation(unittest.TestCase): +# def test_construction_and_json_round_trip(self): +# # Construct using (gid, section_idx, offset) +# loc = CompartmentLocation(42, 3, 0.75) +# self.assertEqual(loc.gid, 42) +# self.assertEqual(loc.section_idx, 3) +# self.assertAlmostEqual(loc.offset, 0.75) + +# # Convert to JSON string +# json_str = loc.toJSON() + +# # Construct from JSON string +# parsed = CompartmentLocation(json_str) +# self.assertEqual(parsed, loc) + +# # Construct directly from JSON list string representation +# loc0 = CompartmentLocation("[42, 3, 0.75]") +# self.assertEqual(loc0, loc) \ No newline at end of file diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 62a79e2..a36ad32 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -18,42 +18,49 @@ using json = nlohmann::json; class CompartmentLocation { - std::uint64_t gid_; - std::uint64_t section_idx_; - double offset_; - - void setGid(int64_t gid) { - if (gid < 0) { - throw SonataError(fmt::format("GID must be non-negative, got {}", gid)); + private: + std::uint64_t gid_; + std::uint64_t section_idx_; + double offset_; + + void setGid(int64_t gid) { + if (gid < 0) { + throw SonataError(fmt::format("GID must be non-negative, got {}", gid)); + } + gid_ = static_cast(gid); } - gid_ = static_cast(gid); - } - void setSectionIdx(int64_t section_idx) { - if (section_idx < 0) { - throw SonataError( - fmt::format("Section index must be non-negative, got {}", section_idx)); + void setSectionIdx(int64_t section_idx) { + if (section_idx < 0) { + throw SonataError( + fmt::format("Section index must be non-negative, got {}", section_idx)); + } + section_idx_ = static_cast(section_idx); } - section_idx_ = static_cast(section_idx); - } - void setOffset(double offset) { - if (offset < 0.0 || offset > 1.0) { - throw SonataError( - fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); + void setOffset(double offset) { + if (offset < 0.0 || offset > 1.0) { + throw SonataError( + fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); + } + offset_ = offset; } - offset_ = offset; - } + + /** + * Copy-construction is explicit and private. Used only for cloning. + */ + explicit CompartmentLocation(const CompartmentLocation& other) = default; public: static constexpr double offsetTolerance = 1e-4; - static constexpr double offsetToleranceInv = 1.0 / offsetTolerance; + // static constexpr double offsetToleranceInv = 1.0 / offsetTolerance; + - explicit CompartmentLocation(int64_t gid, int64_t section_idx, double offset) { + CompartmentLocation(int64_t gid, int64_t section_idx, double offset) { setGid(gid); setSectionIdx(section_idx); setOffset(offset); } - explicit CompartmentLocation(const std::string& content) + CompartmentLocation(const std::string& content) : CompartmentLocation(json::parse(content)) {} CompartmentLocation(const nlohmann::json& j) { @@ -63,8 +70,8 @@ class CompartmentLocation "offset]"); } - setGid(get_uint64_or_throw(j[0])); - setSectionIdx(get_uint64_or_throw(j[1])); + setGid(get_int64_or_throw(j[0])); + setSectionIdx(get_int64_or_throw(j[1])); if (!j[2].is_number()) { throw SonataError("Fourth element (offset) must be a number"); @@ -92,195 +99,232 @@ class CompartmentLocation return gid_ == other.gid_ && section_idx_ == other.section_idx_ && std::abs(offset_ - other.offset_) < offsetTolerance; } -}; - -// Custom hash for CompartmentLocation -struct CompartmentLocationHash { - std::size_t operator()(const CompartmentLocation& loc) const noexcept { - std::size_t h1 = std::hash{}(loc.gid()); - std::size_t h2 = std::hash{}(loc.sectionIdx()); - - // Quantize offset to 4 decimal places - double offset = loc.offset(); - uint64_t quantized_offset = static_cast( - std::round(offset * CompartmentLocation::offsetToleranceInv)); - - std::size_t h3 = std::hash{}(quantized_offset); - - // Combine hashes (boost style) - std::size_t seed = 0; - seed ^= h1 + 0x9e3779b9 + (seed << 6) + (seed >> 2); - seed ^= h2 + 0x9e3779b9 + (seed << 6) + (seed >> 2); - seed ^= h3 + 0x9e3779b9 + (seed << 6) + (seed >> 2); - - return seed; - } -}; - -class CompartmentSet { - std::string population_; - std::vector compartment_locations_; - - public: - explicit CompartmentSet(const std::string& content) - : CompartmentSet(json::parse(content)) {} - - explicit CompartmentSet(const nlohmann::json& j) { - if (!j.is_object()) { - throw SonataError("CompartmentSet must be an object"); - } - - // Extract and check 'population' key once - auto pop_it = j.find("population"); - if (pop_it == j.end() || !pop_it->is_string()) { - throw SonataError("CompartmentSet must contain 'population' key of string type"); - } - population_ = pop_it->get(); - - // Extract and check 'compartment_set' key once - auto comp_it = j.find("compartment_set"); - if (comp_it == j.end() || !comp_it->is_array()) { - throw SonataError("CompartmentSet must contain 'compartment_set' key of array type"); - } - - compartment_locations_.reserve(comp_it->size()); - for (const auto& el : *comp_it) { - compartment_locations_.emplace_back(el); - } - compartment_locations_.shrink_to_fit(); - } - - - Selection gids() const { - std::vector result; - std::unordered_set seen; - - result.reserve(compartment_locations_.size()); - for (const auto& elem : compartment_locations_) { - uint64_t id = elem.gid(); - if (seen.insert(id).second) { // insert returns {iterator, bool} - result.push_back(id); - } - } - sort(result.begin(), result.end()); - return Selection::fromValues(result.begin(), result.end()); + bool operator!=(const CompartmentLocation& other) const { + return !(*this == other); } - const std::string& population() const { - return population_; - } - - std::vector> getCompartmentLocations( - const Selection& selection) const { - std::vector> result; - result.reserve(compartment_locations_.size()); - - if (selection.empty()) { - for (const auto& el : compartment_locations_) { - result.emplace_back(std::make_unique(el)); - } - } else { - for (const auto& el : compartment_locations_) { - if (selection.contains(el.gid())) { - result.emplace_back(std::make_unique(el)); - } - } - } - result.shrink_to_fit(); - - return result; - } - - nlohmann::json to_json() const { - nlohmann::json j; - j["population"] = population_; - - j["compartment_set"] = nlohmann::json::array(); - for (const auto& elem : compartment_locations_) { - j["compartment_set"].push_back(elem.to_json()); - } - - return j; + std::unique_ptr clone() const { + return std::unique_ptr(new CompartmentLocation(*this)); } }; -class CompartmentSets -{ - -std::map compartment_sets_; - public: - explicit CompartmentSets(const json& j) { - if (!j.is_object()) { - throw SonataError("Top level compartment_set must be an object"); - } - for (const auto& el : j.items()) { - compartment_sets_.emplace(el.key(), el.value()); - } - } +// // Custom hash for CompartmentLocation +// struct CompartmentLocationHash { +// std::size_t operator()(const CompartmentLocation& loc) const noexcept { +// std::size_t h1 = std::hash{}(loc.gid()); +// std::size_t h2 = std::hash{}(loc.sectionIdx()); + +// // Quantize offset to 4 decimal places +// double offset = loc.offset(); +// uint64_t quantized_offset = static_cast( +// std::round(offset * CompartmentLocation::offsetToleranceInv)); + +// std::size_t h3 = std::hash{}(quantized_offset); + +// // Combine hashes (boost style) +// std::size_t seed = 0; +// seed ^= h1 + 0x9e3779b9 + (seed << 6) + (seed >> 2); +// seed ^= h2 + 0x9e3779b9 + (seed << 6) + (seed >> 2); +// seed ^= h3 + 0x9e3779b9 + (seed << 6) + (seed >> 2); + +// return seed; +// } +// }; + +// class CompartmentSet { +// std::string population_; +// std::vector compartment_locations_; + +// public: +// CompartmentSet(const std::string& content) +// : CompartmentSet(json::parse(content)) {} + +// CompartmentSet(const nlohmann::json& j) { +// if (!j.is_object()) { +// throw SonataError("CompartmentSet must be an object"); +// } + +// // Extract and check 'population' key once +// auto pop_it = j.find("population"); +// if (pop_it == j.end() || !pop_it->is_string()) { +// throw SonataError("CompartmentSet must contain 'population' key of string type"); +// } +// population_ = pop_it->get(); + +// // Extract and check 'compartment_set' key once +// auto comp_it = j.find("compartment_set"); +// if (comp_it == j.end() || !comp_it->is_array()) { +// throw SonataError("CompartmentSet must contain 'compartment_set' key of array type"); +// } + +// compartment_locations_.reserve(comp_it->size()); +// for (const auto& el : *comp_it) { +// compartment_locations_.emplace_back(el); +// } +// compartment_locations_.shrink_to_fit(); +// } + +// std::size_t size() const { +// return compartment_locations_.size(); +// } + +// CompartmentLocation& operator[](std::size_t index) { +// if (index >= compartment_locations_.size()) { +// throw std::out_of_range("CompartmentSet index out of bounds"); +// } +// return compartment_locations_[index]; +// } + +// std::vector::const_iterator cbegin() const noexcept { +// return compartment_locations_.cbegin(); +// } + +// std::vector::const_iterator cend() const noexcept { +// return compartment_locations_.cend(); +// } + +// Selection gids() const { +// std::vector result; +// std::unordered_set seen; + +// result.reserve(compartment_locations_.size()); +// for (const auto& elem : compartment_locations_) { +// uint64_t id = elem.gid(); +// if (seen.insert(id).second) { // insert returns {iterator, bool} +// result.push_back(id); +// } +// } +// sort(result.begin(), result.end()); +// return Selection::fromValues(result.begin(), result.end()); +// } + +// const std::string& population() const { +// return population_; +// } + +// std::vector locations( +// const Selection& selection) const { +// std::vector result; +// result.reserve(compartment_locations_.size()); + +// if (selection.empty()) { +// for (const auto& el : compartment_locations_) { +// result.emplace_back(el); +// } +// } else { +// for (const auto& el : compartment_locations_) { +// if (selection.contains(el.gid())) { +// result.emplace_back(el); +// } +// } +// } +// result.shrink_to_fit(); + +// return result; +// } + +// nlohmann::json to_json() const { +// nlohmann::json j; +// j["population"] = population_; + +// j["compartment_set"] = nlohmann::json::array(); +// for (const auto& elem : compartment_locations_) { +// j["compartment_set"].push_back(elem.to_json()); +// } + +// return j; +// } +// }; +// class CompartmentSets +// { + +// std::map compartment_sets_; + +// public: +// CompartmentSets(const json& j) { +// if (!j.is_object()) { +// throw SonataError("Top level compartment_set must be an object"); +// } +// for (const auto& el : j.items()) { +// compartment_sets_.emplace(el.key(), el.value()); +// } +// } - static const fs::path& validate_path(const fs::path& path) { - if (!fs::exists(path)) { - throw SonataError(fmt::format("Path does not exist: {}", std::string(path))); - } - return path; - } - - static std::unique_ptr fromFile(const std::string& path_) { - fs::path path(path_); - return std::make_unique(path); - } - - explicit CompartmentSets(const fs::path& path) - : CompartmentSets(json::parse(std::ifstream(validate_path(path)))) {} - - explicit CompartmentSets(const std::string& content) - : CompartmentSets(json::parse(content)) {} - - size_t size() const { - return compartment_sets_.size(); - } - bool contains(const std::string& name) const { - return compartment_sets_.find(name) != compartment_sets_.end(); - } - - std::set names() const { - return getMapKeys(compartment_sets_); - } - - CompartmentSet getCompartmentSet(const std::string& name) const { - auto it = compartment_sets_.find(name); - if (it == compartment_sets_.end()) { - throw SonataError(fmt::format("CompartmentSet '{}' not found", name)); - } - return it->second; - } - - nlohmann::json to_json() const { - nlohmann::json j; - for (const auto& entry : compartment_sets_) { - j[entry.first] = entry.second.to_json(); - } - return j; - } -}; +// static const fs::path& validate_path(const fs::path& path) { +// if (!fs::exists(path)) { +// throw SonataError(fmt::format("Path does not exist: {}", std::string(path))); +// } +// return path; +// } + +// static CompartmentSets fromFile(const std::string& path_) { +// fs::path path(path_); +// return path; +// } + +// CompartmentSets(const fs::path& path) +// : CompartmentSets(json::parse(std::ifstream(validate_path(path)))) {} + +// CompartmentSets(const std::string& content) +// : CompartmentSets(json::parse(content)) {} + +// size_t size() const { +// return compartment_sets_.size(); +// } +// bool contains(const std::string& name) const { +// return compartment_sets_.find(name) != compartment_sets_.end(); +// } + +// std::vector keys() const { +// std::vector result; +// result.reserve(compartment_sets_.size()); // reserve space for efficiency + +// for (const auto& kv : compartment_sets_) { +// result.push_back(kv.first); +// } + +// return result; +// } + +// CompartmentSet& get(const std::string& name) { +// auto it = compartment_sets_.find(name); +// if (it == compartment_sets_.end()) { +// throw SonataError(fmt::format("CompartmentSet '{}' not found", name)); +// } +// return it->second; +// } + +// nlohmann::json to_json() const { +// nlohmann::json j; +// for (const auto& entry : compartment_sets_) { +// j[entry.first] = entry.second.to_json(); +// } +// return j; +// } +// }; } // namespace detail // CompartmentLocation python API - CompartmentLocation::CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset) : impl_(new detail::CompartmentLocation(gid, section_idx, offset)) {} - CompartmentLocation::CompartmentLocation(const std::string& content) : impl_(new detail::CompartmentLocation(content)) {} - CompartmentLocation::CompartmentLocation(std::unique_ptr&& impl) : impl_(std::move(impl)) {} +CompartmentLocation::CompartmentLocation(const CompartmentLocation& other) : impl_(other.impl_->clone()){} +CompartmentLocation& CompartmentLocation::operator=(const CompartmentLocation& other) { + if (this != &other) { + auto tmp = other.impl_->clone(); // create copy first, if it throws impl is not assigned + impl_ = std::move(tmp); // then assign + } + return *this; +} CompartmentLocation::CompartmentLocation(CompartmentLocation&&) noexcept = default; CompartmentLocation& CompartmentLocation::operator=(CompartmentLocation&&) noexcept = default; CompartmentLocation::~CompartmentLocation() = default; @@ -289,6 +333,10 @@ bool CompartmentLocation::operator==(const CompartmentLocation& other) const noe return *impl_ == *(other.impl_); } +bool CompartmentLocation::operator!=(const CompartmentLocation& other) const noexcept { + return *impl_ != *(other.impl_); +} + uint64_t CompartmentLocation::gid() const { return impl_->gid(); } @@ -302,79 +350,111 @@ double CompartmentLocation::offset() const { } std::string CompartmentLocation::toJSON() const { - return impl_->to_json().dump(4); // Pretty print with 4 spaces + return impl_->to_json().dump(); } // CompartmentSet python API -CompartmentSet::CompartmentSet(const std::string& content) - : impl_(new detail::CompartmentSet(content)) {} +// CompartmentSet::CompartmentSet(const std::string& content) +// : impl_(new detail::CompartmentSet(content)) {} +// CompartmentSet::CompartmentSet(std::unique_ptr&& impl) +// : impl_(std::move(impl)) {} +// CompartmentSet::CompartmentSet(detail::CompartmentSet&& impl) +// : CompartmentSet(std::make_unique(impl)) {} + +// CompartmentSet::CompartmentSet(CompartmentSet&&) noexcept = default; +// CompartmentSet& CompartmentSet::operator=(CompartmentSet&&) noexcept = default; +// CompartmentSet::~CompartmentSet() = default; + +// std::size_t CompartmentSet::size() const { +// return impl_->size(); +// } + +// CompartmentLocation CompartmentSet::operator[](std::size_t index) const { +// return (*impl_)[index]; +// } + +// const std::string& CompartmentSet::population() const { +// return impl_->population(); +// } + +// std::vector CompartmentSet::locations( +// const Selection& selection) const { +// std::vector view; +// auto raw_locs = impl_->locations(selection); +// view.reserve(raw_locs.size()); +// for (auto& el : raw_locs) { +// view.emplace_back(el); // take ownership +// } +// return view; +// } + +// Selection CompartmentSet::gids() const { +// return impl_->gids(); +// } + +// std::string CompartmentSet::toJSON() const { +// return impl_->to_json().dump(4); // Pretty print with 4 spaces +// } + +// // CompartmentSets python API + +// CompartmentSets::CompartmentSets(const std::string& content) +// : impl_(new detail::CompartmentSets(content)) {} +// CompartmentSets::CompartmentSets(std::unique_ptr&& impl) +// : impl_(std::move(impl)) {} +// CompartmentSets::CompartmentSets(detail::CompartmentSets&& impl) +// : CompartmentSets(std::make_unique(impl)) {} + +// CompartmentSets::CompartmentSets(CompartmentSets&&) noexcept = default; +// CompartmentSets& CompartmentSets::operator=(CompartmentSets&&) noexcept = default; +// CompartmentSets::~CompartmentSets() = default; + + + +// CompartmentSets CompartmentSets::fromFile(const std::string& path) { +// return detail::CompartmentSets::fromFile(path); +// } + +// size_t CompartmentSets::size() const { +// return impl_->size(); +// } + +// bool CompartmentSets::contains(const std::string& name) const { +// return impl_->contains(name); +// } + +// std::vector CompartmentSets::keys() const { +// return impl_->keys(); // Assuming detail::CompartmentSets has keys() returning std::set +// } + +// CompartmentSet CompartmentSets::get(const std::string& name) const { +// // Assuming impl_->getCompartmentSet returns detail::CompartmentSet by value or reference +// return impl_->get(name); +// } + +// std::vector CompartmentSets::values() const { +// std::vector result; +// result.reserve(size()); +// for (const auto& key : keys()) { +// result.emplace_back(impl_->get(key)); +// } +// return result; +// } + +// std::vector> CompartmentSets::items() const { +// std::vector> result; +// for (const auto& key : keys()) { +// result.emplace_back(key, impl_->get(key)); +// } +// return result; +// } + +// std::string CompartmentSets::toJSON() const { +// return impl_->to_json().dump(4); // Pretty print with 4 spaces +// } -CompartmentSet::CompartmentSet(std::unique_ptr&& impl) - : impl_(std::move(impl)) {} - -CompartmentSet::CompartmentSet(CompartmentSet&&) noexcept = default; -CompartmentSet& CompartmentSet::operator=(CompartmentSet&&) noexcept = default; -CompartmentSet::~CompartmentSet() = default; - -const std::string& CompartmentSet::population() const { - return impl_->population(); -} - -std::vector CompartmentSet::getCompartmentLocations( - const Selection& selection) const { - std::vector view; - auto raw_locs = impl_->getCompartmentLocations(selection); - view.reserve(raw_locs.size()); - for (auto& el : raw_locs) { - view.emplace_back(std::move(el)); // take ownership - } - return view; -} -Selection CompartmentSet::gids() const { - return impl_->gids(); -} - -std::string CompartmentSet::toJSON() const { - return impl_->to_json().dump(4); // Pretty print with 4 spaces -} - -// CompartmentSets python API - -CompartmentSets::CompartmentSets(const std::string& content) - : impl_(new detail::CompartmentSets(content)) {} - -CompartmentSets::CompartmentSets(std::unique_ptr&& impl) - : impl_(std::move(impl)) {} - -CompartmentSets::CompartmentSets(CompartmentSets&&) noexcept = default; -CompartmentSets& CompartmentSets::operator=(CompartmentSets&&) noexcept = default; -CompartmentSets::~CompartmentSets() = default; - -CompartmentSets CompartmentSets::fromFile(const std::string& path) { - return CompartmentSets(detail::CompartmentSets::fromFile(path)); -} - -size_t CompartmentSets::size() const { - return impl_->size(); -} - -bool CompartmentSets::contains(const std::string& name) const { - return impl_->contains(name); -} - -std::set CompartmentSets::names() const { - return impl_->names(); -} - -CompartmentSet CompartmentSets::getCompartmentSet(const std::string& name) { - return CompartmentSet(std::make_unique(impl_->getCompartmentSet(name))); -} - -std::string CompartmentSets::toJSON() const { - return impl_->to_json().dump(4); // Pretty print with 4 spaces -} } // namespace sonata diff --git a/src/selection.cpp b/src/selection.cpp index 0515c48..ae671f3 100644 --- a/src/selection.cpp +++ b/src/selection.cpp @@ -61,7 +61,6 @@ Selection union_(const Ranges& lhs, const Ranges& rhs) { } } // namespace detail - Selection::Selection(Selection::Ranges ranges) : ranges_(std::move(ranges)) { detail::_checkRanges(ranges_); diff --git a/test.py b/test.py index d9b0685..baf9884 100644 --- a/test.py +++ b/test.py @@ -1,9 +1,5 @@ from libsonata import ( CompartmentLocation, - CompartmentSets, - CompartmentSet, - SonataError, - Selection ) def inspect(v): @@ -12,6 +8,11 @@ def inspect(v): # if not i.startswith('__'): print(f' {i} = {getattr(v, i)}') -a = CompartmentSets('{ "CompartmentSet0": { "population": "pop0", "compartment_set": [[0, 10, 0.2], [3, 11, 0.2], [0, 10, 0.201], [1, 11, 0.2]] } }') +# a = CompartmentSets('{ "CompartmentSet0": { "population": "pop0", "compartment_set": [[0, 10, 0.2], [3, 11, 0.2], [0, 10, 0.201], [1, 11, 0.2]] }, "CompartmentSet1": { "population": "pop1", "compartment_set": [[0, 10, 0.2], [3, 11, 0.2], [0, 10, 0.201], [1, 11, 0.2]] } }') +b = CompartmentLocation(1, 2, 0.3) +a = CompartmentLocation(4, 5, 0.6) +b = a -print("CompartmentSet" in a) +print(id(b), b) + +print(id(a), a) diff --git a/tests/data/compartment_sets.json b/tests/data/compartment_sets.json index c9c3402..f141cb8 100644 --- a/tests/data/compartment_sets.json +++ b/tests/data/compartment_sets.json @@ -1,6 +1,6 @@ { - "example_compartment_set": { - "population": "S1nonbarrel_neurons", + "cs1": { + "population": "pop1", "compartment_set": [ [0, 10, 0.1], [0, 10, 0.2], @@ -8,5 +8,9 @@ [2, 3, 0.1], [3, 6, 0.3] ] + }, + "cs0": { + "population": "pop0", + "compartment_set": [] } } \ No newline at end of file diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index 9c44714..e850ada 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -1,137 +1,216 @@ #include #include #include -#include -#include -using namespace bbp::sonata; +#include -TEST_CASE("CompartmentLocation: construction and JSON round-trip") { - CompartmentLocation loc(42, 3, 0.75); - REQUIRE(loc.gid() == 42); - REQUIRE(loc.sectionIdx() == 3); - REQUIRE(loc.offset() == Approx(0.75)); +using namespace bbp::sonata; - std::string json = loc.toJSON(); - CompartmentLocation parsed(json); - REQUIRE(parsed == loc); -} +TEST_CASE("CompartmentLocation public API") { -TEST_CASE("CompartmentSet: valid JSON parsing and access") { - std::string json = R"({ - "population": "exc", - "compartment_set": [ - [1, 0, 0.0], - [2, 1, 0.5], - [1, 2, 1.0] - ] - })"; - - CompartmentSet set(json); - REQUIRE(set.population() == "exc"); - - auto gids = set.gids(); - REQUIRE(gids.flatten().size() == 2); - REQUIRE(gids.contains(1)); - REQUIRE(gids.contains(2)); - - auto locs = set.getCompartmentLocations(); - REQUIRE(locs.size() == 3); - REQUIRE(locs[0].gid() == 1); - REQUIRE(locs[1].sectionIdx() == 1); - REQUIRE(locs[2].offset() == Approx(1.0)); -} + SECTION("Construct from valid gid, section_idx, offset") { + CompartmentLocation loc(1, 10, 0.5); + REQUIRE(loc.gid() == 1); + REQUIRE(loc.sectionIdx() == 10); + REQUIRE(loc.offset() == Approx(0.5)); + } -TEST_CASE("CompartmentSets: parse multiple sets and query") { - std::string json = R"({ - "setA": { - "population": "pop1", - "compartment_set": [[10, 0, 0.0]] - }, - "setB": { - "population": "pop1", - "compartment_set": [[11, 1, 0.5], [12, 2, 0.7]] - } - })"; - - CompartmentSets sets(json); - REQUIRE(sets.size() == 2); - REQUIRE(sets.contains("setA")); - REQUIRE(sets.contains("setB")); - - auto names = sets.names(); - REQUIRE(names.size() == 2); - REQUIRE(names.find("setA") != names.end()); - REQUIRE(names.find("setB") != names.end()); - - auto setA = sets.getCompartmentSet("setA"); - REQUIRE(setA.gids().contains(10)); - - auto setB = sets.getCompartmentSet("setB"); - auto locs = setB.getCompartmentLocations(); - REQUIRE(locs.size() == 2); - REQUIRE(locs[1].offset() == Approx(0.7)); -} + SECTION("Construct from valid JSON string") { + std::string json_str = "[1, 10, 0.5]"; + CompartmentLocation loc(json_str); + REQUIRE(loc.gid() == 1); + REQUIRE(loc.sectionIdx() == 10); + REQUIRE(loc.offset() == Approx(0.5)); + } -TEST_CASE("CompartmentSets: round-trip serialization") { - std::string json = R"({ - "cs0": { - "population": "P", - "compartment_set": [[1, 1, 0.1]] - } - })"; - - CompartmentSets sets(json); - std::string out = sets.toJSON(); - CompartmentSets reloaded(out); - - REQUIRE(reloaded.contains("cs0")); - auto set = reloaded.getCompartmentSet("cs0"); - auto locs = set.getCompartmentLocations(); - REQUIRE(locs.size() == 1); - REQUIRE(locs[0].gid() == 1); - REQUIRE(locs[0].offset() == Approx(0.1)); -} + SECTION("Invalid JSON string throws") { + REQUIRE_THROWS_AS(CompartmentLocation("{\"gid\": 1}"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("[1, 2]"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("[1, 2, 0.1, 1]"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("[\"a\", 2, 0.5]"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("[1, \"a\", 0.5]"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("[1, 2, \"a\"]"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("[1, 2, 2.0]"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("[1, 2, -0.1]"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("[-1, 2, 0.1]"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("[1, -2, 0.1]"), SonataError); + } -TEST_CASE("CompartmentSets: load from valid file") { - CompartmentSets sets = CompartmentSets::fromFile("./data/compartment_sets.json"); - REQUIRE(sets.size() > 0); + SECTION("Equality operators") { + CompartmentLocation loc1(1, 10, 0.5); + CompartmentLocation loc2(1, 10, 0.5); + CompartmentLocation loc3(1, 10, 0.6); + CompartmentLocation loc4(1, 10, 0.5000001); - for (const auto& name : sets.names()) { - auto set = sets.getCompartmentSet(name); - REQUIRE_FALSE(set.getCompartmentLocations().empty()); + REQUIRE(loc1 == loc2); + REQUIRE_FALSE(loc1 != loc2); + REQUIRE(loc1 != loc3); + REQUIRE(loc1 == loc4); } -} + SECTION("Copy constructor and assignment") { + CompartmentLocation original(2, 20, 0.7); + CompartmentLocation copy_constructed(original); + CompartmentLocation copy_assigned(0, 0, 0); + copy_assigned = original; -TEST_CASE("CompartmentSet and CompartmentSets: throw SonataError on malformed JSON") { - SECTION("CompartmentSet: missing offset") { - std::string json = R"({ "population": "P", "compartment_set": [ [1, 1] ] })"; - CHECK_THROWS_AS(CompartmentSet(json), SonataError); + REQUIRE(copy_constructed == original); + REQUIRE(copy_assigned == original); } - SECTION("CompartmentSet: malformed array structure") { - std::string json = R"({ "population": "P", "compartment_set": [ 1, 2, 3 ] })"; - CHECK_THROWS_AS(CompartmentSet(json), SonataError); - } + SECTION("Move constructor and assignment") { + CompartmentLocation original(3, 30, 0.8); + CompartmentLocation moved_constructed(std::move(original)); - SECTION("CompartmentSet: missing population field") { - std::string json = R"({ "compartment_set": [ [1, 1, 0.5] ] })"; - CHECK_THROWS_AS(CompartmentSet(json), SonataError); - } + REQUIRE(moved_constructed.gid() == 3); + REQUIRE(moved_constructed.sectionIdx() == 30); + REQUIRE(moved_constructed.offset() == Approx(0.8)); - SECTION("CompartmentSets: missing compartment_set in one entry") { - std::string json = R"({ "set1": { "population": "P" } })"; - CHECK_THROWS_AS(CompartmentSets(json), SonataError); - } + CompartmentLocation another(0, 0, 0); + another = std::move(moved_constructed); - SECTION("CompartmentSets: invalid inner structure") { - std::string json = R"({ "set1": [1, 2, 3] })"; - CHECK_THROWS_AS(CompartmentSets(json), SonataError); + REQUIRE(another.gid() == 3); + REQUIRE(another.sectionIdx() == 30); + REQUIRE(another.offset() == Approx(0.8)); } - SECTION("CompartmentSets: malformed JSON string") { - std::string json = R"({ "set1": { "population": "P", "compartment_set": [ [1, 1 ] })"; // unclosed bracket - CHECK_THROWS_AS(CompartmentSets(json), std::exception); + SECTION("toJSON returns valid JSON string") { + CompartmentLocation loc(4, 40, 0.9); + auto json_str = loc.toJSON(); + + auto json = nlohmann::json::parse(json_str); + REQUIRE(json.is_array()); + REQUIRE(json[0] == 4); + REQUIRE(json[1] == 40); + REQUIRE(json[2] == Approx(0.9)); } } + + +// TEST_CASE("CompartmentSets: fail on invalid JSON strings") { +// // Top level must be an object +// REQUIRE_THROWS_AS(CompartmentSets("1"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets("[\"array\"]"), SonataError); + +// // Each CompartmentSet must be an object with 'population' and 'compartment_set' keys +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": 1 })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": "string" })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": null })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": true })"), SonataError); + +// // Missing keys +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "compartment_set": [] } })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0" } })"), SonataError); + +// // Invalid types +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": 123, "compartment_set": [] } })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": null, "compartment_set": [] } })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": "not an array" } })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": 123 } })"), SonataError); + +// // Invalid compartment_set elements +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [1] } })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 2]] } })"), SonataError); + +// // Wrong types inside compartment_set elements +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [["not uint64", 0, 0.5]] } })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, "not uint64", 0.5]] } })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 0, "not a number"]] } })"), SonataError); + +// // Location out of bounds +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 0, -0.1]] } })"), SonataError); +// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 0, 1.1]] } })"), SonataError); +// } + +// TEST_CASE("CompartmentSets: load from valid JSON file") { +// const auto sets = CompartmentSets::fromFile("./data/compartment_sets.json"); + +// REQUIRE(sets.size() == 2); + +// SECTION("Key presence") { +// REQUIRE(sets.contains("cs0")); +// REQUIRE(sets.contains("cs1")); +// } + +// SECTION("Key ordering") { +// const auto keys = sets.keys(); +// REQUIRE(keys == std::vector{"cs0", "cs1"}); +// } + +// SECTION("Values and items") { +// const auto values = sets.values(); +// REQUIRE(values.size() == 2); +// REQUIRE(values[0].population() == "pop0"); +// REQUIRE(values[1].population() == "pop1"); + +// const auto items = sets.items(); +// REQUIRE(items.size() == 2); +// REQUIRE(items[0].first == "cs0"); +// REQUIRE(items[0].second.population() == "pop0"); +// } + +// SECTION("Get by name") { +// const auto cs0 = sets.get("cs0"); +// REQUIRE(cs0.population() == "pop0"); +// } +// } + +// TEST_CASE("CompartmentSets: round-trip serialization") { +// std::string json = R"({ +// "cs0": { +// "population": "P", +// "compartment_set": [[1, 1, 0.1]] +// } +// })"; + +// CompartmentSets sets(json); +// std::string out = sets.toJSON(); +// CompartmentSets reloaded(out); + +// REQUIRE(reloaded.contains("cs0")); +// auto set = reloaded.get("cs0"); +// auto locs = set.locations(); +// REQUIRE(locs.size() == 1); +// REQUIRE(locs[0].gid() == 1); +// REQUIRE(locs[0].offset() == Approx(0.1)); +// } + + +// TEST_CASE("CompartmentLocation: construction and JSON round-trip") { +// CompartmentLocation loc(42, 3, 0.75); +// REQUIRE(loc.gid() == 42); +// REQUIRE(loc.sectionIdx() == 3); +// REQUIRE(loc.offset() == Approx(0.75)); + +// std::string json = loc.toJSON(); +// CompartmentLocation parsed(json); +// REQUIRE(parsed == loc); +// CompartmentLocation loc0("[42, 3, 0.75]"); +// REQUIRE(loc == loc0); +// } + +// TEST_CASE("CompartmentSet: valid JSON parsing and access") { +// std::string json = R"({ +// "population": "exc", +// "compartment_set": [ +// [1, 0, 0.0], +// [2, 1, 0.5], +// [1, 2, 1.0] +// ] +// })"; + +// CompartmentSet set(json); +// REQUIRE(set.population() == "exc"); + +// auto gids = set.gids(); +// REQUIRE(gids.flatten().size() == 2); +// REQUIRE(gids.contains(1)); +// REQUIRE(gids.contains(2)); + +// auto locs = set.getCompartmentLocations(); +// REQUIRE(locs.size() == 3); +// REQUIRE(locs[0].gid() == 1); +// REQUIRE(locs[1].sectionIdx() == 1); +// REQUIRE(locs[2].offset() == Approx(1.0)); +// } + From 807ce4ce9dffe9509326f4f8d5568037ee47d9c0 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Tue, 3 Jun 2025 17:08:40 +0200 Subject: [PATCH 13/44] before python api for compartmentSet --- CMakeLists.txt | 18 +-- include/bbp/sonata/compartment_sets.h | 36 +---- python/tests/test_compartment_sets.py | 69 ++++++++ python/tests/test_compartmentsets.py | 209 ------------------------ src/compartment_sets.cpp | 222 ++++++++++++++++++++++---- 5 files changed, 266 insertions(+), 288 deletions(-) create mode 100644 python/tests/test_compartment_sets.py delete mode 100644 python/tests/test_compartmentsets.py diff --git a/CMakeLists.txt b/CMakeLists.txt index ed5033e..a6a8a0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,22 +89,8 @@ endif() # Targets # ============================================================================= -set(SONATA_SRC - src/common.cpp - src/config.cpp - src/edge_index.cpp - src/edges.cpp - src/hdf5_mutex.cpp - src/hdf5_reader.cpp - src/node_sets.cpp - src/compartment_sets.cpp - src/nodes.cpp - src/population.cpp - src/report_reader.cpp - src/selection.cpp - src/utils.cpp - ${CMAKE_CURRENT_BINARY_DIR}/src/version.cpp - ) +file(GLOB SONATA_SRC "src/*.cpp") +list(APPEND SONATA_SRC "${CMAKE_CURRENT_BINARY_DIR}/src/version.cpp") configure_file ( ${CMAKE_CURRENT_SOURCE_DIR}/src/version.cpp.in diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 2ebc7b4..ab0241e 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -7,9 +7,7 @@ namespace sonata { namespace detail { class CompartmentLocation; class CompartmentSet; -class CompartmentSets; } // namespace detail - /** * CompartmentLocation public API. * @@ -51,47 +49,15 @@ class SONATA_API CompartmentLocation // class SONATA_API CompartmentSet // { // public: -// /** -// * Create CompartmentSet from JSON -// * -// * See also: -// * TODO -// * -// * \param content is the JSON compartment_set value -// * \throw if content cannot be parsed -// */ -// CompartmentSet(const std::string& content); -// CompartmentSet(std::unique_ptr&& impl); -// CompartmentSet(detail::CompartmentSet&& impl); -// CompartmentSet(CompartmentSet&&) noexcept; -// CompartmentSet(const CompartmentSet& other) = delete; -// CompartmentSet& operator=(CompartmentSet&&) noexcept; -// ~CompartmentSet(); + // std::size_t size() const; - // CompartmentLocation operator[](std::size_t index) const; - -// /** -// * Population name -// */ // const std::string& population() const; - -// /** -// * Get the CompartmentLocations. -// */ // std::vector locations( // const Selection& selection = Selection({})) const; - -// /** -// * Return the gids of the compartment locations. -// */ // Selection gids() const; - -// /** -// * Return the nodesets as a JSON string. -// */ // std::string toJSON() const; // private: diff --git a/python/tests/test_compartment_sets.py b/python/tests/test_compartment_sets.py new file mode 100644 index 0000000..6a16b51 --- /dev/null +++ b/python/tests/test_compartment_sets.py @@ -0,0 +1,69 @@ +import json +import os +import unittest + +from libsonata import ( + CompartmentLocation, +) + +PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), + '../../tests/data') + +class TestCompartmentLocation(unittest.TestCase): + def test_constructor_from_values(self): + loc = CompartmentLocation(4, 40, 0.9) + self.assertEqual(loc.gid, 4) + self.assertEqual(loc.section_idx, 40) + self.assertAlmostEqual(loc.offset, 0.9) + + def test_constructor_from_string(self): + loc = CompartmentLocation("[4, 40, 0.9]") + self.assertEqual(loc.gid, 4) + self.assertEqual(loc.section_idx, 40) + self.assertAlmostEqual(loc.offset, 0.9) + + def test_toJSON(self): + loc = CompartmentLocation(4, 40, 0.9) + self.assertEqual(loc.toJSON(), "[4,40,0.9]") + + def test_equality(self): + loc1 = CompartmentLocation(4, 40, 0.9) + loc2 = CompartmentLocation("[4, 40, 0.9]") + loc3 = CompartmentLocation(5, 40, 0.9) + self.assertEqual(loc1, loc2) + self.assertNotEqual(loc1, loc3) + + def test_repr_and_str(self): + loc = CompartmentLocation(4, 40, 0.9) + expected = "CompartmentLocation(4, 40, 0.9)" + self.assertEqual(repr(loc), expected) + self.assertEqual(str(loc), repr(loc)) # str should delegate to repr + + def test_iterable(self): + loc = CompartmentLocation(4, 40, 0.9) + gid, section_idx, offset = loc + self.assertEqual(gid, 4) + self.assertEqual(section_idx, 40) + self.assertAlmostEqual(offset, 0.9) + + def test_assignment_creates_copy(self): + loc1 = CompartmentLocation(1, 2, 0.3) + loc2 = loc1 # This is a reference assignment in Python + self.assertEqual(loc1, loc2) + + # Now mutate loc1 and check if loc2 is affected — which it will be, unless toJSON etc. are implemented with deep semantics + self.assertIs(loc1, loc2) # They reference the same object + + def test_explicit_copy(self): + import copy + loc1 = CompartmentLocation(1, 2, 0.3) + loc2 = copy.copy(loc1) + self.assertEqual(loc1, loc2) + self.assertIsNot(loc1, loc2) # Ensure they’re distinct objects + + def test_deepcopy(self): + import copy + loc1 = CompartmentLocation(1, 2, 0.3) + loc2 = copy.deepcopy(loc1) + self.assertEqual(loc1, loc2) + self.assertIsNot(loc1, loc2) diff --git a/python/tests/test_compartmentsets.py b/python/tests/test_compartmentsets.py deleted file mode 100644 index af081d5..0000000 --- a/python/tests/test_compartmentsets.py +++ /dev/null @@ -1,209 +0,0 @@ -import json -import os -import unittest - -from libsonata import ( - CompartmentLocation, -# CompartmentSet, -# CompartmentSets, -# SonataError, -) - -PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), - '../../tests/data') - -class TestCompartmentLocation(unittest.TestCase): - def test_constructor_from_values(self): - loc = CompartmentLocation(4, 40, 0.9) - self.assertEqual(loc.gid, 4) - self.assertEqual(loc.section_idx, 40) - self.assertAlmostEqual(loc.offset, 0.9) - - def test_constructor_from_string(self): - loc = CompartmentLocation("[4, 40, 0.9]") - self.assertEqual(loc.gid, 4) - self.assertEqual(loc.section_idx, 40) - self.assertAlmostEqual(loc.offset, 0.9) - - def test_toJSON(self): - loc = CompartmentLocation(4, 40, 0.9) - self.assertEqual(loc.toJSON(), "[4,40,0.9]") - - def test_equality(self): - loc1 = CompartmentLocation(4, 40, 0.9) - loc2 = CompartmentLocation("[4, 40, 0.9]") - loc3 = CompartmentLocation(5, 40, 0.9) - self.assertEqual(loc1, loc2) - self.assertNotEqual(loc1, loc3) - - def test_repr_and_str(self): - loc = CompartmentLocation(4, 40, 0.9) - expected = "CompartmentLocation(4, 40, 0.9)" - self.assertEqual(repr(loc), expected) - self.assertEqual(str(loc), repr(loc)) # str should delegate to repr - - def test_iterable(self): - loc = CompartmentLocation(4, 40, 0.9) - gid, section_idx, offset = loc - self.assertEqual(gid, 4) - self.assertEqual(section_idx, 40) - self.assertAlmostEqual(offset, 0.9) - - def test_assignment_creates_copy(self): - loc1 = CompartmentLocation(1, 2, 0.3) - loc2 = loc1 # This is a reference assignment in Python - self.assertEqual(loc1, loc2) - - # Now mutate loc1 and check if loc2 is affected — which it will be, unless toJSON etc. are implemented with deep semantics - self.assertIs(loc1, loc2) # They reference the same object - - def test_explicit_copy(self): - import copy - loc1 = CompartmentLocation(1, 2, 0.3) - loc2 = copy.copy(loc1) - self.assertEqual(loc1, loc2) - self.assertIsNot(loc1, loc2) # Ensure they’re distinct objects - - def test_deepcopy(self): - import copy - loc1 = CompartmentLocation(1, 2, 0.3) - loc2 = copy.deepcopy(loc1) - self.assertEqual(loc1, loc2) - self.assertIsNot(loc1, loc2) - - -# class TestCompartmentSetsFailure(unittest.TestCase): -# def test_CorrectStructure(self): -# # Top level must be an object -# self.assertRaises(SonataError, CompartmentSets, "1") -# self.assertRaises(SonataError, CompartmentSets, '["array"]') - -# # Each CompartmentSet must be an object with population and compartment_set keys -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": 1 }') -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": "string" }') -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": null }') -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": true }') - -# def test_MissingPopulationOrCompartmentSet(self): -# # Missing population key -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "compartment_set": [] } }') -# # Missing compartment_set key -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop" } }') - -# def test_InvalidPopulationType(self): -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": 123, "compartment_set": [] } }') -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": null, "compartment_set": [] } }') - -# def test_InvalidCompartmentSetType(self): -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": "not an array" } }') -# self.assertRaises(SonataError, CompartmentSets, '{ "CompartmentSet0": { "population": "pop", "compartment_set": 123 } }') - -# def test_InvalidCompartmentLocationStructure(self): -# # Each compartment_set element must be an array of 3 elements [gid, section_idx, location] - -# # Not an array -# self.assertRaises(SonataError, CompartmentSets, ''' -# { -# "CompartmentSet0": { -# "population": "pop", -# "compartment_set": [ 1 ] -# } -# } -# ''') - -# # Array with wrong size -# self.assertRaises(SonataError, CompartmentSets, ''' -# { -# "CompartmentSet0": { -# "population": "pop", -# "compartment_set": [ [1, 0] ] -# } -# } -# ''') - -# # Wrong types inside element -# self.assertRaises(SonataError, CompartmentSets, ''' -# { -# "CompartmentSet0": { -# "population": "pop", -# "compartment_set": [ ["not uint64", 0, 0.5] ] -# } -# } -# ''') - - -# self.assertRaises(SonataError, CompartmentSets, ''' -# { -# "CompartmentSet0": { -# "population": "pop", -# "compartment_set": [ [1, "not uint64", 0.5] ] -# } -# } -# ''') - -# self.assertRaises(SonataError, CompartmentSets, ''' -# { -# "CompartmentSet0": { -# "population": "pop", -# "compartment_set": [ [1, 0, "not a number"] ] -# } -# } -# ''') - -# # Location out of bounds -# self.assertRaises(SonataError, CompartmentSets, ''' -# { -# "CompartmentSet0": { -# "population": "pop", -# "compartment_set": [ [1, 0, -0.1] ] -# } -# } -# ''') - -# self.assertRaises(SonataError, CompartmentSets, ''' -# { -# "CompartmentSet0": { -# "population": "pop", -# "compartment_set": [ [1, 0, 1.1] ] -# } -# } -# ''') - -# def test_MissingFile(self): -# self.assertRaises(SonataError, CompartmentSets.from_file, 'this/file/does/not/exist') - - -# class TestLoadValidCompartmentSets(unittest.TestCase): -# def test_valid_compartment_sets_file(self): -# # Load a valid compartment sets file -# file_path = os.path.join(PATH, 'compartment_sets.json') -# cs = CompartmentSets.from_file(file_path) - -# self.assertIsInstance(cs, CompartmentSets) -# self.assertEqual(len(cs), 2) -# self.assertEqual(['cs0', 'cs1'], cs.keys()) -# self.assertIn('cs0', cs) -# self.assertFalse('cs2' in cs) -# self.assertIsInstance(cs['cs0'], CompartmentSet) -# self.assertEqual(cs['cs0'].population, 'pop0') -# self.assertEqual(len(cs.values()), 2) -# self.assertEqual(len(cs.items()), 2) - -# class TestCompartmentLocation(unittest.TestCase): -# def test_construction_and_json_round_trip(self): -# # Construct using (gid, section_idx, offset) -# loc = CompartmentLocation(42, 3, 0.75) -# self.assertEqual(loc.gid, 42) -# self.assertEqual(loc.section_idx, 3) -# self.assertAlmostEqual(loc.offset, 0.75) - -# # Convert to JSON string -# json_str = loc.toJSON() - -# # Construct from JSON string -# parsed = CompartmentLocation(json_str) -# self.assertEqual(parsed, loc) - -# # Construct directly from JSON list string representation -# loc0 = CompartmentLocation("[42, 3, 0.75]") -# self.assertEqual(loc0, loc) \ No newline at end of file diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index a36ad32..4c7e473 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -1,11 +1,11 @@ -#include "../extlib/filesystem.hpp" -#include -#include -#include + +#include "../extlib/filesystem.hpp" #include "utils.h" // readFile +#include + #include namespace bbp { namespace sonata { @@ -16,6 +16,7 @@ namespace detail { using json = nlohmann::json; + class CompartmentLocation { private: @@ -44,16 +45,13 @@ class CompartmentLocation offset_ = offset; } - /** - * Copy-construction is explicit and private. Used only for cloning. - */ - explicit CompartmentLocation(const CompartmentLocation& other) = default; - public: static constexpr double offsetTolerance = 1e-4; // static constexpr double offsetToleranceInv = 1.0 / offsetTolerance; - + CompartmentLocation(const CompartmentLocation& other) = default; + CompartmentLocation(CompartmentLocation&&) noexcept = default; + CompartmentLocation& operator=(CompartmentLocation&&) noexcept = default; CompartmentLocation(int64_t gid, int64_t section_idx, double offset) { setGid(gid); setSectionIdx(section_idx); @@ -108,28 +106,195 @@ class CompartmentLocation } }; -// // Custom hash for CompartmentLocation -// struct CompartmentLocationHash { -// std::size_t operator()(const CompartmentLocation& loc) const noexcept { -// std::size_t h1 = std::hash{}(loc.gid()); -// std::size_t h2 = std::hash{}(loc.sectionIdx()); -// // Quantize offset to 4 decimal places -// double offset = loc.offset(); -// uint64_t quantized_offset = static_cast( -// std::round(offset * CompartmentLocation::offsetToleranceInv)); +class CompartmentSet { +public: + using container_t = std::vector; + class FilteredIterator; +private: + // Private constructor for filter factory method -// std::size_t h3 = std::hash{}(quantized_offset); + std::string population_; + container_t compartment_locations_; + -// // Combine hashes (boost style) -// std::size_t seed = 0; -// seed ^= h1 + 0x9e3779b9 + (seed << 6) + (seed >> 2); -// seed ^= h2 + 0x9e3779b9 + (seed << 6) + (seed >> 2); -// seed ^= h3 + 0x9e3779b9 + (seed << 6) + (seed >> 2); + /** + * Copy-construction is private. Used only for cloning. + */ + CompartmentSet(const CompartmentSet& other) = default; + CompartmentSet(const std::string& population, const container_t& compartment_locations): population_(population), compartment_locations_(compartment_locations) {} -// return seed; -// } -// }; +public: + + // Construct from JSON string (delegates to JSON constructor) + explicit CompartmentSet(const std::string& content) + : CompartmentSet(nlohmann::json::parse(content)) {} + + // Construct from JSON object + explicit CompartmentSet(const nlohmann::json& j) { + if (!j.is_object()) { + throw SonataError("CompartmentSet must be an object"); + } + + auto pop_it = j.find("population"); + if (pop_it == j.end() || !pop_it->is_string()) { + throw SonataError("CompartmentSet must contain 'population' key of string type"); + } + population_ = pop_it->get(); + + auto comp_it = j.find("compartment_set"); + if (comp_it == j.end() || !comp_it->is_array()) { + throw SonataError("CompartmentSet must contain 'compartment_set' key of array type"); + } + + compartment_locations_.reserve(comp_it->size()); + for (auto&& el : *comp_it) { + compartment_locations_.emplace_back(std::forward(el)); + } + compartment_locations_.shrink_to_fit(); + } + + ~CompartmentSet() = default; + CompartmentSet& operator=(const CompartmentSet&) = delete; + CompartmentSet(CompartmentSet&&) noexcept = default; + CompartmentSet& operator=(CompartmentSet&&) noexcept = default; + + class FilteredIterator { + public: + using base_iterator = CompartmentSet::container_t::const_iterator; + using value_type = detail::CompartmentLocation; + using reference = const value_type&; + using pointer = const value_type*; + using difference_type = std::ptrdiff_t; + using iterator_category = std::input_iterator_tag; + private: + base_iterator current_; + base_iterator end_; + bbp::sonata::Selection selection_; // copied + + void skip_to_valid() { + while (current_ != end_) { + if (selection_.empty() || selection_.contains(current_->gid())) { + break; + } + ++current_; + } + } + + public: + + FilteredIterator(base_iterator current, + base_iterator end, + bbp::sonata::Selection selection) + : current_(current), end_(end), selection_(std::move(selection)) { + skip_to_valid(); + } + + reference operator*() const { + return *current_; + } + + pointer operator->() const { + return &(*current_); + } + + FilteredIterator& operator++() { + ++current_; + skip_to_valid(); + return *this; + } + + FilteredIterator operator++(int) { + FilteredIterator tmp = *this; + ++(*this); + return tmp; + } + + bool operator==(const FilteredIterator& other) const { + return current_ == other.current_; + } + + bool operator!=(const FilteredIterator& other) const { + return !(*this == other); + } + }; + + std::pair + filtered_crange(bbp::sonata::Selection selection = Selection({})) const { + FilteredIterator begin_it(compartment_locations_.cbegin(), + compartment_locations_.cend(), + selection); + FilteredIterator end_it(compartment_locations_.cend(), + compartment_locations_.cend(), + std::move(selection)); + return {begin_it, end_it}; + } + + // Size with optional filter + std::size_t size(const bbp::sonata::Selection& selection = bbp::sonata::Selection({})) const { + if (selection.empty()) { + return compartment_locations_.size(); + } + return static_cast(std::count_if(compartment_locations_.begin(), + compartment_locations_.end(), + [&](const CompartmentLocation& loc) { + return selection.contains(loc.gid()); + })); + } + + const CompartmentLocation& operator[](std::size_t index) const { + return compartment_locations_.at(index); + } + + Selection gids() const { + std::vector result; + std::unordered_set seen; + + result.reserve(compartment_locations_.size()); + for (const auto& elem : compartment_locations_) { + uint64_t id = elem.gid(); + if (seen.insert(id).second) { // insert returns {iterator, bool} + result.push_back(id); + } + } + sort(result.begin(), result.end()); + return Selection::fromValues(result.begin(), result.end()); + } + + const std::string& population() const { + return population_; + } + + nlohmann::json to_json() const { + nlohmann::json j; + j["population"] = population_; + + j["compartment_set"] = nlohmann::json::array(); + for (const auto& elem : compartment_locations_) { + j["compartment_set"].push_back(elem.to_json()); + } + + return j; + } + + std::unique_ptr clone() const { + return std::unique_ptr(new CompartmentSet(*this)); + } + + std::unique_ptr filter(const bbp::sonata::Selection& selection = bbp::sonata::Selection({})) const { + if (selection.empty()) { + return clone(); + } + std::vector filtered; + filtered.reserve(compartment_locations_.size()); + for (const auto& el : compartment_locations_) { + if (selection.contains(el.gid())) { + filtered.emplace_back(el); + } + } + return std::unique_ptr(new CompartmentSet(population_, std::move(filtered))); + } +}; // class CompartmentSet { // std::string population_; @@ -308,6 +473,7 @@ class CompartmentLocation } // namespace detail // CompartmentLocation python API + CompartmentLocation::CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset) From b529ff4d742d44183d85356f52227c9c22d08f73 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Tue, 3 Jun 2025 21:03:21 +0200 Subject: [PATCH 14/44] WIP decoupled iterator --- include/bbp/sonata/compartment_sets.h | 55 ++++++++ src/compartment_sets.cpp | 177 ++++++++++++++++---------- 2 files changed, 164 insertions(+), 68 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index ab0241e..b8f7984 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -7,6 +7,7 @@ namespace sonata { namespace detail { class CompartmentLocation; class CompartmentSet; +class CompartmentSetFilteredIterator; } // namespace detail /** * CompartmentLocation public API. @@ -46,6 +47,60 @@ class SONATA_API CompartmentLocation }; +/** + * CompartmentSet public API. + * + * This class represents a set of compartment locations associated with a neuron population. + * Each compartment is uniquely defined by a (gid, section_idx, offset) triplet. + * This API supports filtering based on a gid selection. + */ +class SONATA_API CompartmentSet +{ + public: + + CompartmentSet() = delete; + + explicit CompartmentSet(const std::string& json_content); + explicit CompartmentSet(std::shared_ptr&& impl); + + // class FilteredIterator { + // public: + // explicit FilteredIterator(std::unique_ptr impl); + // ~FilteredIterator(); + // // CompartmentLocation operator*() const; + // // FilteredIterator& operator++(); // prefix ++ + // // FilteredIterator operator++(int); // postfix ++ + // // bool operator==(const FilteredIterator& other) const; + // // bool operator!=(const FilteredIterator& other) const; + + // private: + // std::unique_ptr impl_; + // }; + + // std::pair + // filteredRange(Selection selection = bbp::sonata::Selection({})) const; + + /// Size of the set, optionally filtered by selection + std::size_t size(const bbp::sonata::Selection& selection = bbp::sonata::Selection({})) const; + + /// Population name + const std::string& population() const; + + /// Access element by index. It returns a copy! + CompartmentLocation operator[](std::size_t index) const; + + bbp::sonata::Selection gids() const; + + CompartmentSet filter(const bbp::sonata::Selection& selection = bbp::sonata::Selection({})) const; + + /// Serialize to JSON string + std::string toJSON() const; + + private: + std::shared_ptr impl_; +}; + + // class SONATA_API CompartmentSet // { // public: diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 4c7e473..85770f6 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -106,7 +106,65 @@ class CompartmentLocation } }; +class CompartmentSetFilteredIterator { +public: + using base_iterator = std::vector::const_iterator; + using value_type = detail::CompartmentLocation; + using reference = const value_type&; + using pointer = const value_type*; + using difference_type = std::ptrdiff_t; + using iterator_category = std::input_iterator_tag; +private: + base_iterator current_; + base_iterator end_; + bbp::sonata::Selection selection_; // copied + + void skip_to_valid() { + while (current_ != end_) { + if (selection_.empty() || selection_.contains(current_->gid())) { + break; + } + ++current_; + } + } + +public: + + CompartmentSetFilteredIterator(base_iterator current, + base_iterator end, + bbp::sonata::Selection selection) + : current_(current), end_(end), selection_(std::move(selection)) { + skip_to_valid(); + } + + reference operator*() const { + return *current_; + } + + pointer operator->() const { + return &(*current_); + } + + CompartmentSetFilteredIterator& operator++() { + ++current_; + skip_to_valid(); + return *this; + } + + CompartmentSetFilteredIterator operator++(int) { + CompartmentSetFilteredIterator tmp = *this; + ++(*this); + return tmp; + } + + bool operator==(const CompartmentSetFilteredIterator& other) const { + return current_ == other.current_; + } + bool operator!=(const CompartmentSetFilteredIterator& other) const { + return !(*this == other); + } +}; class CompartmentSet { public: using container_t = std::vector; @@ -159,72 +217,12 @@ class CompartmentSet { CompartmentSet(CompartmentSet&&) noexcept = default; CompartmentSet& operator=(CompartmentSet&&) noexcept = default; - class FilteredIterator { - public: - using base_iterator = CompartmentSet::container_t::const_iterator; - using value_type = detail::CompartmentLocation; - using reference = const value_type&; - using pointer = const value_type*; - using difference_type = std::ptrdiff_t; - using iterator_category = std::input_iterator_tag; - private: - base_iterator current_; - base_iterator end_; - bbp::sonata::Selection selection_; // copied - - void skip_to_valid() { - while (current_ != end_) { - if (selection_.empty() || selection_.contains(current_->gid())) { - break; - } - ++current_; - } - } - - public: - - FilteredIterator(base_iterator current, - base_iterator end, - bbp::sonata::Selection selection) - : current_(current), end_(end), selection_(std::move(selection)) { - skip_to_valid(); - } - - reference operator*() const { - return *current_; - } - - pointer operator->() const { - return &(*current_); - } - - FilteredIterator& operator++() { - ++current_; - skip_to_valid(); - return *this; - } - - FilteredIterator operator++(int) { - FilteredIterator tmp = *this; - ++(*this); - return tmp; - } - - bool operator==(const FilteredIterator& other) const { - return current_ == other.current_; - } - - bool operator!=(const FilteredIterator& other) const { - return !(*this == other); - } - }; - - std::pair + std::pair filtered_crange(bbp::sonata::Selection selection = Selection({})) const { - FilteredIterator begin_it(compartment_locations_.cbegin(), + CompartmentSetFilteredIterator begin_it(compartment_locations_.cbegin(), compartment_locations_.cend(), selection); - FilteredIterator end_it(compartment_locations_.cend(), + CompartmentSetFilteredIterator end_it(compartment_locations_.cend(), compartment_locations_.cend(), std::move(selection)); return {begin_it, end_it}; @@ -242,8 +240,8 @@ class CompartmentSet { })); } - const CompartmentLocation& operator[](std::size_t index) const { - return compartment_locations_.at(index); + std::unique_ptr operator[](std::size_t index) const { + return compartment_locations_.at(index).clone(); } Selection gids() const { @@ -472,7 +470,7 @@ class CompartmentSet { } // namespace detail -// CompartmentLocation python API +// CompartmentLocation public API CompartmentLocation::CompartmentLocation(const int64_t gid, const int64_t section_idx, @@ -519,7 +517,50 @@ std::string CompartmentLocation::toJSON() const { return impl_->to_json().dump(); } -// CompartmentSet python API +// CompartmentSetFilteredIterator public API + +// CompartmentSet::FilteredIterator::FilteredIterator(std::unique_ptr impl) +// : impl_(std::move(impl)) {} +// CompartmentSet::FilteredIterator::~FilteredIterator() = default; + +// CompartmentSet public API + +CompartmentSet::CompartmentSet(const std::string& json_content) + : impl_(std::make_shared(json_content)) {} + +CompartmentSet::CompartmentSet(std::shared_ptr&& impl) + : impl_(std::move(impl)) {} + + +const std::string& CompartmentSet::population() const { + return impl_->population(); +} + +std::size_t CompartmentSet::size(const bbp::sonata::Selection& selection) const { + return impl_->size(selection); +} + +CompartmentLocation CompartmentSet::operator[](std::size_t index) const { + return CompartmentLocation((*impl_)[index]); +} + +bbp::sonata::Selection CompartmentSet::gids() const { + return impl_->gids(); +} + +CompartmentSet CompartmentSet::filter(const bbp::sonata::Selection& selection) const { + return CompartmentSet(impl_->filter(selection)); +} + + +std::string CompartmentSet::toJSON() const { + return impl_->to_json().dump(); +} + + + + +// CompartmentSet public API // CompartmentSet::CompartmentSet(const std::string& content) // : impl_(new detail::CompartmentSet(content)) {} @@ -563,7 +604,7 @@ std::string CompartmentLocation::toJSON() const { // return impl_->to_json().dump(4); // Pretty print with 4 spaces // } -// // CompartmentSets python API +// // CompartmentSets public API // CompartmentSets::CompartmentSets(const std::string& content) // : impl_(new detail::CompartmentSets(content)) {} From b9936706f310ef2a7e3ef902a6ea72e0991198a3 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Tue, 3 Jun 2025 21:06:21 +0200 Subject: [PATCH 15/44] WIP --- include/bbp/sonata/compartment_sets.h | 30 +++++++++++++++------------ src/compartment_sets.cpp | 6 +++--- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index b8f7984..cb4dc75 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -54,6 +54,22 @@ class SONATA_API CompartmentLocation * Each compartment is uniquely defined by a (gid, section_idx, offset) triplet. * This API supports filtering based on a gid selection. */ + + +class SONATA_API CompartmentSetFilteredIterator { +public: + explicit CompartmentSetFilteredIterator(std::unique_ptr impl); + ~CompartmentSetFilteredIterator(); + // CompartmentLocation operator*() const; + // FilteredIterator& operator++(); // prefix ++ + // FilteredIterator operator++(int); // postfix ++ + // bool operator==(const FilteredIterator& other) const; + // bool operator!=(const FilteredIterator& other) const; + +private: + std::unique_ptr> impl_; +}; + class SONATA_API CompartmentSet { public: @@ -63,19 +79,7 @@ class SONATA_API CompartmentSet explicit CompartmentSet(const std::string& json_content); explicit CompartmentSet(std::shared_ptr&& impl); - // class FilteredIterator { - // public: - // explicit FilteredIterator(std::unique_ptr impl); - // ~FilteredIterator(); - // // CompartmentLocation operator*() const; - // // FilteredIterator& operator++(); // prefix ++ - // // FilteredIterator operator++(int); // postfix ++ - // // bool operator==(const FilteredIterator& other) const; - // // bool operator!=(const FilteredIterator& other) const; - - // private: - // std::unique_ptr impl_; - // }; + // std::pair // filteredRange(Selection selection = bbp::sonata::Selection({})) const; diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 85770f6..34415a0 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -519,9 +519,9 @@ std::string CompartmentLocation::toJSON() const { // CompartmentSetFilteredIterator public API -// CompartmentSet::FilteredIterator::FilteredIterator(std::unique_ptr impl) -// : impl_(std::move(impl)) {} -// CompartmentSet::FilteredIterator::~FilteredIterator() = default; +CompartmentSetFilteredIterator::CompartmentSetFilteredIterator(std::unique_ptr impl) + : impl_(std::move(impl)) {} +CompartmentSetFilteredIterator::~CompartmentSetFilteredIterator() = default; // CompartmentSet public API From 3aed13cc9edcf32f1704e23ae267e00cdf7b8658 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 4 Jun 2025 01:17:43 +0200 Subject: [PATCH 16/44] WIP --- include/bbp/sonata/compartment_sets.h | 47 ++++++++------- src/compartment_sets.cpp | 64 +++++++++++++++++++- tests/test_compartment_sets.cpp | 84 +++++++++++++++++++++++++++ 3 files changed, 173 insertions(+), 22 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index cb4dc75..91662fc 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -46,30 +46,39 @@ class SONATA_API CompartmentLocation std::unique_ptr impl_; }; - -/** - * CompartmentSet public API. - * - * This class represents a set of compartment locations associated with a neuron population. - * Each compartment is uniquely defined by a (gid, section_idx, offset) triplet. - * This API supports filtering based on a gid selection. - */ - - class SONATA_API CompartmentSetFilteredIterator { public: + using iterator_category = std::input_iterator_tag; + using value_type = CompartmentLocation; + using difference_type = std::ptrdiff_t; + using pointer = void; // dereference returns by value + using reference = CompartmentLocation; // dereference returns by value + explicit CompartmentSetFilteredIterator(std::unique_ptr impl); + CompartmentSetFilteredIterator(const CompartmentSetFilteredIterator& other); + CompartmentSetFilteredIterator& operator=(const CompartmentSetFilteredIterator& other); + CompartmentSetFilteredIterator(CompartmentSetFilteredIterator&&) noexcept; + CompartmentSetFilteredIterator& operator=(CompartmentSetFilteredIterator&&) noexcept; ~CompartmentSetFilteredIterator(); - // CompartmentLocation operator*() const; - // FilteredIterator& operator++(); // prefix ++ - // FilteredIterator operator++(int); // postfix ++ - // bool operator==(const FilteredIterator& other) const; - // bool operator!=(const FilteredIterator& other) const; + + /// Dereference operator. It makes a copy! + CompartmentLocation operator*() const; + CompartmentSetFilteredIterator& operator++(); // prefix ++ + CompartmentSetFilteredIterator operator++(int); // postfix ++ + bool operator==(const CompartmentSetFilteredIterator& other) const; + bool operator!=(const CompartmentSetFilteredIterator& other) const; private: - std::unique_ptr> impl_; + std::unique_ptr impl_; }; +/** + * CompartmentSet public API. + * + * This class represents a set of compartment locations associated with a neuron population. + * Each compartment is uniquely defined by a (gid, section_idx, offset) triplet. + * This API supports filtering based on a gid selection. + */ class SONATA_API CompartmentSet { public: @@ -79,10 +88,8 @@ class SONATA_API CompartmentSet explicit CompartmentSet(const std::string& json_content); explicit CompartmentSet(std::shared_ptr&& impl); - - - // std::pair - // filteredRange(Selection selection = bbp::sonata::Selection({})) const; + std::pair + filtered_crange(bbp::sonata::Selection selection = bbp::sonata::Selection({})) const; /// Size of the set, optionally filtered by selection std::size_t size(const bbp::sonata::Selection& selection = bbp::sonata::Selection({})) const; diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 34415a0..dcca177 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -164,6 +164,10 @@ class CompartmentSetFilteredIterator { bool operator!=(const CompartmentSetFilteredIterator& other) const { return !(*this == other); } + + std::unique_ptr clone() const { + return std::make_unique(*this); + } }; class CompartmentSet { public: @@ -233,6 +237,7 @@ class CompartmentSet { if (selection.empty()) { return compartment_locations_.size(); } + return static_cast(std::count_if(compartment_locations_.begin(), compartment_locations_.end(), [&](const CompartmentLocation& loc) { @@ -519,10 +524,54 @@ std::string CompartmentLocation::toJSON() const { // CompartmentSetFilteredIterator public API +// Constructor CompartmentSetFilteredIterator::CompartmentSetFilteredIterator(std::unique_ptr impl) : impl_(std::move(impl)) {} + +// Copy constructor +CompartmentSetFilteredIterator::CompartmentSetFilteredIterator(const CompartmentSetFilteredIterator& other) + : impl_(other.impl_ ? other.impl_->clone() : nullptr) {} + +// Copy assignment operator +CompartmentSetFilteredIterator& CompartmentSetFilteredIterator::operator=(const CompartmentSetFilteredIterator& other) { + if (this != &other) { + impl_ = other.impl_ ? other.impl_->clone() : nullptr; + } + return *this; +} + +// Move constructor +CompartmentSetFilteredIterator::CompartmentSetFilteredIterator(CompartmentSetFilteredIterator&&) noexcept = default; + +// Move assignment operator +CompartmentSetFilteredIterator& CompartmentSetFilteredIterator::operator=(CompartmentSetFilteredIterator&&) noexcept = default; + + CompartmentSetFilteredIterator::~CompartmentSetFilteredIterator() = default; +CompartmentLocation CompartmentSetFilteredIterator::operator*() const { + return CompartmentLocation(impl_->operator*().clone()); +} + +CompartmentSetFilteredIterator& CompartmentSetFilteredIterator::operator++() { + ++(*impl_); + return *this; +} + +CompartmentSetFilteredIterator CompartmentSetFilteredIterator::operator++(int) { + CompartmentSetFilteredIterator tmp(std::make_unique(*impl_)); + ++(*impl_); + return tmp; +} + +bool CompartmentSetFilteredIterator::operator==(const CompartmentSetFilteredIterator& other) const { + return *impl_ == *other.impl_; +} + +bool CompartmentSetFilteredIterator::operator!=(const CompartmentSetFilteredIterator& other) const { + return !(*this == other); +} + // CompartmentSet public API CompartmentSet::CompartmentSet(const std::string& json_content) @@ -532,14 +581,25 @@ CompartmentSet::CompartmentSet(std::shared_ptr&& impl) : impl_(std::move(impl)) {} -const std::string& CompartmentSet::population() const { - return impl_->population(); +std::pair +CompartmentSet::filtered_crange(bbp::sonata::Selection selection) const { + const auto internal_result = impl_->filtered_crange(std::move(selection)); + + // Wrap clones of detail iterators in public API iterators + return { + CompartmentSetFilteredIterator(internal_result.first.clone()), + CompartmentSetFilteredIterator(internal_result.second.clone()) + }; } std::size_t CompartmentSet::size(const bbp::sonata::Selection& selection) const { return impl_->size(selection); } +const std::string& CompartmentSet::population() const { + return impl_->population(); +} + CompartmentLocation CompartmentSet::operator[](std::size_t index) const { return CompartmentLocation((*impl_)[index]); } diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index e850ada..40a4296 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -5,6 +5,7 @@ #include using namespace bbp::sonata; +using json = nlohmann::json; TEST_CASE("CompartmentLocation public API") { @@ -87,6 +88,89 @@ TEST_CASE("CompartmentLocation public API") { } +TEST_CASE("CompartmentSet public API") { + + // Example JSON representing a CompartmentSet (adjust to match real expected format) + std::string json_content = R"( + { + "population": "test_population", + "compartment_set": [ + [1, 10, 0.5], + [2, 20, 0.25], + [2, 20, 0.25], + [3, 30, 0.75] + ] + } + )"; + + SECTION("Construct from JSON string, round-trip serialization") { + CompartmentSet cs(json_content); + + REQUIRE(cs.population() == "test_population"); + REQUIRE(cs.size() == 4); + + // Access elements by index + REQUIRE(cs[0] == CompartmentLocation(1, 10, 0.5)); + REQUIRE(cs[1] == CompartmentLocation(2, 20, 0.25)); + REQUIRE(cs[2] == CompartmentLocation(2, 20, 0.25)); + REQUIRE(cs[3] == CompartmentLocation(3, 30, 0.75)); + REQUIRE_THROWS_AS(cs[4], std::out_of_range); + REQUIRE(cs.toJSON() == json::parse(json_content).dump()); + } + + SECTION("Size with selection filter") { + CompartmentSet cs(json_content); + + REQUIRE(cs.size(Selection::fromValues({1, 2})) == 3); + REQUIRE(cs.size(Selection::fromValues({3, 8, 9, 10, 13})) == 1); + REQUIRE(cs.size(Selection::fromValues({999})) == 0); + } + + // SECTION("Filtered iteration") { + // CompartmentSet cs(json_content); + // Selection sel({1, 3}); + + // auto [begin, end] = cs.filtered_crange(sel); + + // std::vector gids; + // for (auto it = begin; it != end; ++it) { + // gids.push_back(it->gid()); + // } + + // REQUIRE(gids.size() == 2); + // REQUIRE((gids == std::vector{1, 3})); + // } + + // SECTION("Filter returns subset") { + // CompartmentSet cs(json_content); + // Selection sel({1, 2}); + // auto filtered = cs.filter(sel); + + // REQUIRE(filtered.size() == 2); + + // // Check filtered compartments only contain gids 1 and 2 + // auto gids = filtered.gids(); + // REQUIRE(gids.contains(1)); + // REQUIRE(gids.contains(2)); + // REQUIRE(!gids.contains(3)); + // } + + // SECTION("toJSON serialization") { + // CompartmentSet cs(json_content); + // std::string json_out = cs.toJSON(); + + // // The output should contain the population name (basic check) + // REQUIRE(json_out.find("test_population") != std::string::npos); + + // // It should contain all gids from the set (basic check) + // REQUIRE(json_out.find("1") != std::string::npos); + // REQUIRE(json_out.find("2") != std::string::npos); + // REQUIRE(json_out.find("3") != std::string::npos); + // } +} + + + // TEST_CASE("CompartmentSets: fail on invalid JSON strings") { // // Top level must be an object // REQUIRE_THROWS_AS(CompartmentSets("1"), SonataError); From 03b4a398a20489b7822f95bcc4fa3a945f388c3f Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 4 Jun 2025 11:20:46 +0200 Subject: [PATCH 17/44] CompartmentSet ready for python api --- include/bbp/sonata/compartment_sets.h | 4 +- src/compartment_sets.cpp | 2 +- tests/test_compartment_sets.cpp | 101 ++++++++++++++++---------- 3 files changed, 66 insertions(+), 41 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 91662fc..5d0fd85 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -23,7 +23,7 @@ class CompartmentSetFilteredIterator; class SONATA_API CompartmentLocation { public: - CompartmentLocation() = delete; + CompartmentLocation(); CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset); explicit CompartmentLocation(const std::string& content); explicit CompartmentLocation(std::unique_ptr&& impl); @@ -63,6 +63,8 @@ class SONATA_API CompartmentSetFilteredIterator { /// Dereference operator. It makes a copy! CompartmentLocation operator*() const; + /// Arrow operator is voluntarely disabled because we can only return copies of CompartmentLocation. + /// In any way we need to find a location to store a temp CompartmentLocation and memory leaks become possible. CompartmentSetFilteredIterator& operator++(); // prefix ++ CompartmentSetFilteredIterator operator++(int); // postfix ++ bool operator==(const CompartmentSetFilteredIterator& other) const; diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index dcca177..bccc084 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -476,7 +476,7 @@ class CompartmentSet { } // namespace detail // CompartmentLocation public API - +CompartmentLocation::CompartmentLocation() = default; CompartmentLocation::CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset) diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index 40a4296..3312f3b 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -97,8 +97,8 @@ TEST_CASE("CompartmentSet public API") { "compartment_set": [ [1, 10, 0.5], [2, 20, 0.25], - [2, 20, 0.25], - [3, 30, 0.75] + [3, 30, 0.75], + [2, 20, 0.25] ] } )"; @@ -112,12 +112,42 @@ TEST_CASE("CompartmentSet public API") { // Access elements by index REQUIRE(cs[0] == CompartmentLocation(1, 10, 0.5)); REQUIRE(cs[1] == CompartmentLocation(2, 20, 0.25)); - REQUIRE(cs[2] == CompartmentLocation(2, 20, 0.25)); - REQUIRE(cs[3] == CompartmentLocation(3, 30, 0.75)); + REQUIRE(cs[2] == CompartmentLocation(3, 30, 0.75)); + REQUIRE(cs[3] == CompartmentLocation(2, 20, 0.25)); REQUIRE_THROWS_AS(cs[4], std::out_of_range); REQUIRE(cs.toJSON() == json::parse(json_content).dump()); } + SECTION("JSON constructor throws on invalid input") { + // Not an object (array instead) + REQUIRE_THROWS_AS(CompartmentSet("[1, 2, 3]"), SonataError); + + // Missing population key + REQUIRE_THROWS_AS( + CompartmentSet(R"({"compartment_set": []})"), + SonataError + ); + + // population not a string + REQUIRE_THROWS_AS( + CompartmentSet(R"({"population": 123, "compartment_set": []})"), + SonataError + ); + + // Missing compartment_set key + REQUIRE_THROWS_AS( + CompartmentSet(R"({"population": "test_population"})"), + SonataError + ); + + // compartment_set not an array + REQUIRE_THROWS_AS( + CompartmentSet(R"({"population": "test_population", "compartment_set": "not an array"})"), + SonataError + ); + } + + SECTION("Size with selection filter") { CompartmentSet cs(json_content); @@ -126,47 +156,40 @@ TEST_CASE("CompartmentSet public API") { REQUIRE(cs.size(Selection::fromValues({999})) == 0); } - // SECTION("Filtered iteration") { - // CompartmentSet cs(json_content); - // Selection sel({1, 3}); - - // auto [begin, end] = cs.filtered_crange(sel); - - // std::vector gids; - // for (auto it = begin; it != end; ++it) { - // gids.push_back(it->gid()); - // } - - // REQUIRE(gids.size() == 2); - // REQUIRE((gids == std::vector{1, 3})); - // } + SECTION("Filtered iteration") { + CompartmentSet cs(json_content); - // SECTION("Filter returns subset") { - // CompartmentSet cs(json_content); - // Selection sel({1, 2}); - // auto filtered = cs.filter(sel); + auto pp = cs.filtered_crange(); - // REQUIRE(filtered.size() == 2); + std::vector gids; + for (auto it = pp.first; it != pp.second; ++it) { + gids.push_back((*it).gid()); + } - // // Check filtered compartments only contain gids 1 and 2 - // auto gids = filtered.gids(); - // REQUIRE(gids.contains(1)); - // REQUIRE(gids.contains(2)); - // REQUIRE(!gids.contains(3)); - // } + REQUIRE(gids.size() == 4); + REQUIRE((gids == std::vector{1, 2, 3, 2})); + gids.clear(); + for (auto it = cs.filtered_crange(Selection::fromValues({2, 3})).first; it != pp.second; ++it) { + gids.push_back((*it).gid()); + } + REQUIRE(gids.size() == 3); + REQUIRE((gids == std::vector{2, 3, 2})); + } - // SECTION("toJSON serialization") { - // CompartmentSet cs(json_content); - // std::string json_out = cs.toJSON(); + SECTION("Filter returns subset") { + CompartmentSet cs(json_content); + auto filtered = cs.filter(Selection::fromValues({2, 3})); - // // The output should contain the population name (basic check) - // REQUIRE(json_out.find("test_population") != std::string::npos); + REQUIRE(filtered.size() == 3); - // // It should contain all gids from the set (basic check) - // REQUIRE(json_out.find("1") != std::string::npos); - // REQUIRE(json_out.find("2") != std::string::npos); - // REQUIRE(json_out.find("3") != std::string::npos); - // } + // Check filtered compartments only contain gids 1 and 2 + auto gids = filtered.gids().flatten(); + REQUIRE(gids == std::vector({2, 3})); + auto no_filtered = cs.filter(); + REQUIRE(no_filtered.size() == 4); + auto no_filtered_gids = no_filtered.gids().flatten(); + REQUIRE(no_filtered_gids == std::vector({1, 2, 3})); + } } From 4f8e4173426b77e763bfe9ae606a16b606995830 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 4 Jun 2025 13:01:35 +0200 Subject: [PATCH 18/44] added pybinds and tests for CompartmentSet --- python/bindings.cpp | 69 ++++++++++++++++----------- python/generated/docstrings.h | 10 +++- python/libsonata/__init__.py | 2 + python/tests/test_compartment_sets.py | 68 ++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 31 deletions(-) diff --git a/python/bindings.cpp b/python/bindings.cpp index 8671ee4..62ba0e7 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -577,35 +577,46 @@ PYBIND11_MODULE(_libsonata, m) { [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); }); - - - - // py::class_(m, "CompartmentSet", "CompartmentSet") - // .def(py::init()) - // .def_property_readonly("population", - // &CompartmentSet::population, - // DOC_COMPARTMENTSET(population)) - // .def("__len__", [](const CompartmentSet& self) { - // return self.size(); - // }) - // .def("__getitem__", [](const CompartmentSet& self, py::ssize_t i) { - // if (i < 0) { - // i += static_cast(self.size()); - // } - // if (i < 0 || static_cast(i) >= self.size()) { - // throw py::index_error("Index out of range"); - // } - // return self[static_cast(i)]; - // }, py::arg("index")) - // .def("gids", &CompartmentSet::gids) - // .def( - // "locations", - // [](const CompartmentSet& self, const Selection& sel = Selection({})) { - // return self.locations(sel); - // }, - // py::arg("selection") = Selection({}), - // DOC_COMPARTMENTSET(locations)) - // .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)); + + py::class_(m, "CompartmentSet") + .def(py::init()) + .def_property_readonly("population", &CompartmentSet::population, DOC_COMPARTMENTSET(population)) + .def("size", + py::overload_cast(&CompartmentSet::size, py::const_), + py::arg("selection") = bbp::sonata::Selection({}), DOC_COMPARTMENTSET(size)) + .def("gids", &CompartmentSet::gids, DOC_COMPARTMENTSET(gids)) + .def("filter", + &CompartmentSet::filter, + py::arg("selection") = bbp::sonata::Selection({}), + DOC_COMPARTMENTSET(filter)) + .def("filtered_iter", + [](const CompartmentSet& self, const bbp::sonata::Selection& sel) { + auto range = self.filtered_crange(sel); + return py::make_iterator(range.first, range.second); + }, + py::arg("selection") = bbp::sonata::Selection({}), + py::keep_alive<0, 1>(), + DOC_COMPARTMENTSET(filteredIter)) + .def("__len__", [](const CompartmentSet& self) { + return self.size(); + }) + .def("__getitem__", [](const CompartmentSet& self, py::ssize_t i) { + if (i < 0) { + i += static_cast(self.size()); + } + if (i < 0 || static_cast(i) >= self.size()) { + throw py::index_error("Index out of range"); + } + return self[static_cast(i)]; + }, py::arg("index"), + DOC_COMPARTMENTSET(getitem)) + .def("__iter__", + [](const CompartmentSet& self) { + auto range = self.filtered_crange(bbp::sonata::Selection({})); + return py::make_iterator(range.first, range.second); + }, + py::keep_alive<0, 1>()) + .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)); // py::class_(m, "CompartmentSets", "CompartmentSets") // .def(py::init()) diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 8a89f21..8095b38 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -391,11 +391,17 @@ static const char *__doc_bbp_sonata_CompartmentLocation_toJSON = R"doc(Return th static const char *__doc_bbp_sonata_CompartmentSet_population = R"doc(Population name)doc"; +static const char *__doc_bbp_sonata_CompartmentSet_toJSON = R"doc(Return the compartment set as a JSON string.)doc"; + +static const char *__doc_bbp_sonata_CompartmentSet_size = R"doc(Return the size of the set, optionally filtered by selection.)doc"; + +static const char *__doc_bbp_sonata_CompartmentSet_getitem = R"doc("Get a CompartmentLocation by index. Creates a copy of the object.")doc"; + static const char *__doc_bbp_sonata_CompartmentSet_gids = R"doc(Gids in the list of CompartmentLocations.)doc"; -static const char *__doc_bbp_sonata_CompartmentSet_locations = R"doc(Get the list of CompartmentLocations.)doc"; +static const char *__doc_bbp_sonata_CompartmentSet_filter = R"doc(Filter the compartment set based on a selection.)doc"; -static const char *__doc_bbp_sonata_CompartmentSet_toJSON = R"doc(Return the compartment set as a JSON string.)doc"; +static const char *__doc_bbp_sonata_CompartmentSet_filteredIter = R"doc(Iterator over CompartmentLocations filtered by selection.)doc"; static const char *__doc_bbp_sonata_CompartmentSets_keys = R"doc(Return the keys of the CompartmentSets)doc"; diff --git a/python/libsonata/__init__.py b/python/libsonata/__init__.py index 6b441a7..cca83aa 100644 --- a/python/libsonata/__init__.py +++ b/python/libsonata/__init__.py @@ -15,6 +15,7 @@ NodePopulation, NodeSets, CompartmentLocation, + CompartmentSet, NodeStorage, Selection, SomaDataFrame, @@ -40,6 +41,7 @@ "NodePopulation", "NodeSets", "CompartmentLocation", + "CompartmentSet", "NodeStorage", "Selection", "SomaDataFrame", diff --git a/python/tests/test_compartment_sets.py b/python/tests/test_compartment_sets.py index 6a16b51..d9e4215 100644 --- a/python/tests/test_compartment_sets.py +++ b/python/tests/test_compartment_sets.py @@ -4,6 +4,8 @@ from libsonata import ( CompartmentLocation, + CompartmentSet, + Selection ) PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), @@ -67,3 +69,69 @@ def test_deepcopy(self): loc2 = copy.deepcopy(loc1) self.assertEqual(loc1, loc2) self.assertIsNot(loc1, loc2) + +class TestCompartmentSet(unittest.TestCase): + def setUp(self): + self.json = '''{ + "population": "pop0", + "compartment_set": [ + [1, 10, 0.5], + [2, 20, 0.25], + [3, 30, 0.75], + [2, 20, 0.25] + ] + }''' + self.cs = CompartmentSet(self.json) + + def test_population_property(self): + self.assertIsInstance(self.cs.population, str) + self.assertEqual(self.cs.population, "pop0") + + def test_size(self): + self.assertEqual(self.cs.size(), 4) + self.assertEqual(self.cs.size([1, 2]), 3) + self.assertEqual(self.cs.size(Selection([[2, 3]])), 2) + + def test_len_dunder(self): + self.assertEqual(len(self.cs), 4) + + def test_getitem(self): + loc = self.cs[0] + self.assertEqual((loc.gid, loc.section_idx, loc.offset), (1, 10, 0.5)) + + def test_getitem_negative_index(self): + loc = self.cs[-1] + self.assertEqual((loc.gid, loc.section_idx, loc.offset), (2, 20, 0.25)) + + def test_getitem_out_of_bounds_raises(self): + with self.assertRaises(IndexError): + _ = self.cs[10] + with self.assertRaises(IndexError): + _ = self.cs[-10] + + def test_iterators(self): + gids = [loc.gid for loc in self.cs] + self.assertEqual(gids, [1, 2, 3, 2]) + gids = [loc.gid for loc in self.cs.filtered_iter([2, 3])] + self.assertEqual(gids, [2, 3, 2]) + conv_to_list = list(self.cs.filtered_iter([2, 3])) + self.assertTrue(all(isinstance(i, CompartmentLocation) for i in conv_to_list)) + + def test_gids(self): + gids = self.cs.gids() + self.assertEqual(gids, Selection([1, 2, 3])) + + def test_filter_identity(self): + filtered = self.cs.filter() + self.assertEqual(filtered.size(), 4) + filtered = self.cs.filter(Selection([1, 2])) + self.assertEqual(filtered.size(), 3) + + def test_toJSON_roundtrip(self): + json_out = self.cs.toJSON() + cs2 = CompartmentSet(json_out) + self.assertEqual(len(cs2), 4) + self.assertEqual(cs2.population, self.cs.population) + self.assertEqual([tuple(loc) for loc in cs2], [tuple(loc) for loc in self.cs]) + + From e5bd13adb67c35e9bf0e6087221453b50cd4e334 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 4 Jun 2025 15:48:16 +0200 Subject: [PATCH 19/44] completed detail::CompartmentSets --- src/compartment_sets.cpp | 224 +++++++++++---------------------------- 1 file changed, 64 insertions(+), 160 deletions(-) diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index bccc084..3c62008 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -298,180 +298,84 @@ class CompartmentSet { return std::unique_ptr(new CompartmentSet(population_, std::move(filtered))); } }; +class CompartmentSets +{ +private: + std::map> data_; -// class CompartmentSet { -// std::string population_; -// std::vector compartment_locations_; - -// public: -// CompartmentSet(const std::string& content) -// : CompartmentSet(json::parse(content)) {} - -// CompartmentSet(const nlohmann::json& j) { -// if (!j.is_object()) { -// throw SonataError("CompartmentSet must be an object"); -// } - -// // Extract and check 'population' key once -// auto pop_it = j.find("population"); -// if (pop_it == j.end() || !pop_it->is_string()) { -// throw SonataError("CompartmentSet must contain 'population' key of string type"); -// } -// population_ = pop_it->get(); - -// // Extract and check 'compartment_set' key once -// auto comp_it = j.find("compartment_set"); -// if (comp_it == j.end() || !comp_it->is_array()) { -// throw SonataError("CompartmentSet must contain 'compartment_set' key of array type"); -// } - -// compartment_locations_.reserve(comp_it->size()); -// for (const auto& el : *comp_it) { -// compartment_locations_.emplace_back(el); -// } -// compartment_locations_.shrink_to_fit(); -// } - -// std::size_t size() const { -// return compartment_locations_.size(); -// } - -// CompartmentLocation& operator[](std::size_t index) { -// if (index >= compartment_locations_.size()) { -// throw std::out_of_range("CompartmentSet index out of bounds"); -// } -// return compartment_locations_[index]; -// } - -// std::vector::const_iterator cbegin() const noexcept { -// return compartment_locations_.cbegin(); -// } - -// std::vector::const_iterator cend() const noexcept { -// return compartment_locations_.cend(); -// } - -// Selection gids() const { -// std::vector result; -// std::unordered_set seen; - -// result.reserve(compartment_locations_.size()); -// for (const auto& elem : compartment_locations_) { -// uint64_t id = elem.gid(); -// if (seen.insert(id).second) { // insert returns {iterator, bool} -// result.push_back(id); -// } -// } -// sort(result.begin(), result.end()); -// return Selection::fromValues(result.begin(), result.end()); -// } - -// const std::string& population() const { -// return population_; -// } - -// std::vector locations( -// const Selection& selection) const { -// std::vector result; -// result.reserve(compartment_locations_.size()); - -// if (selection.empty()) { -// for (const auto& el : compartment_locations_) { -// result.emplace_back(el); -// } -// } else { -// for (const auto& el : compartment_locations_) { -// if (selection.contains(el.gid())) { -// result.emplace_back(el); -// } -// } -// } -// result.shrink_to_fit(); - -// return result; -// } - -// nlohmann::json to_json() const { -// nlohmann::json j; -// j["population"] = population_; +public: + CompartmentSets(const json& j) { + if (!j.is_object()) { + throw SonataError("Top level compartment_set must be an object"); + } -// j["compartment_set"] = nlohmann::json::array(); -// for (const auto& elem : compartment_locations_) { -// j["compartment_set"].push_back(elem.to_json()); -// } + for (const auto& el : j.items()) { + data_.emplace(el.key(), std::make_shared(el.value())); + } + } -// return j; -// } -// }; -// class CompartmentSets -// { - -// std::map compartment_sets_; - -// public: -// CompartmentSets(const json& j) { -// if (!j.is_object()) { -// throw SonataError("Top level compartment_set must be an object"); -// } -// for (const auto& el : j.items()) { -// compartment_sets_.emplace(el.key(), el.value()); -// } -// } - -// static const fs::path& validate_path(const fs::path& path) { -// if (!fs::exists(path)) { -// throw SonataError(fmt::format("Path does not exist: {}", std::string(path))); -// } -// return path; -// } + static const fs::path& validate_path(const fs::path& path) { + if (!fs::exists(path)) { + throw SonataError(fmt::format("Path does not exist: {}", std::string(path))); + } + return path; + } -// static CompartmentSets fromFile(const std::string& path_) { -// fs::path path(path_); -// return path; -// } + CompartmentSets(const fs::path& path) + : CompartmentSets(json::parse(std::ifstream(validate_path(path)))) {} -// CompartmentSets(const fs::path& path) -// : CompartmentSets(json::parse(std::ifstream(validate_path(path)))) {} + static CompartmentSets fromFile(const std::string& path_) { + fs::path path(path_); + return path; + } -// CompartmentSets(const std::string& content) -// : CompartmentSets(json::parse(content)) {} + CompartmentSets(const std::string& content) + : CompartmentSets(json::parse(content)) {} -// size_t size() const { -// return compartment_sets_.size(); -// } -// bool contains(const std::string& name) const { -// return compartment_sets_.find(name) != compartment_sets_.end(); -// } -// std::vector keys() const { -// std::vector result; -// result.reserve(compartment_sets_.size()); // reserve space for efficiency + std::shared_ptr at(const std::string& key) const { + return data_.at(key); + } -// for (const auto& kv : compartment_sets_) { -// result.push_back(kv.first); -// } + std::size_t size() const { + return data_.size(); + } -// return result; -// } + bool contains(const std::string& key) const { + return data_.find(key) != data_.end(); + } -// CompartmentSet& get(const std::string& name) { -// auto it = compartment_sets_.find(name); -// if (it == compartment_sets_.end()) { -// throw SonataError(fmt::format("CompartmentSet '{}' not found", name)); -// } -// return it->second; -// } + std::vector keys() const { + std::vector result; + result.reserve(data_.size()); + std::transform(data_.begin(), data_.end(), std::back_inserter(result), + [](const auto& kv) { return kv.first; }); + return result; + } -// nlohmann::json to_json() const { -// nlohmann::json j; -// for (const auto& entry : compartment_sets_) { -// j[entry.first] = entry.second.to_json(); -// } -// return j; -// } -// }; + std::vector> values() const { + std::vector> result; + result.reserve(data_.size()); + std::transform(data_.begin(), data_.end(), std::back_inserter(result), + [](const auto& kv) { return kv.second; }); + return result; + } + std::vector>> items() const { + std::vector>> result; + result.reserve(data_.size()); + std::copy(data_.begin(), data_.end(), std::back_inserter(result)); + return result; + } + nlohmann::json to_json() const { + nlohmann::json j; + for (const auto& entry : data_) { + j[entry.first] = entry.second->to_json(); + } + return j; + } +}; } // namespace detail From 07a7bf8c5bf84bcff04a3ac1e552e26b4b949a77 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 4 Jun 2025 16:34:22 +0200 Subject: [PATCH 20/44] add equality CompartmentSet --- include/bbp/sonata/compartment_sets.h | 79 ++++++----- python/bindings.cpp | 6 + python/tests/test_compartment_sets.py | 19 +++ src/compartment_sets.cpp | 186 ++++++++++++-------------- tests/test_compartment_sets.cpp | 55 ++++++++ 5 files changed, 205 insertions(+), 140 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 5d0fd85..64db3e6 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -6,8 +6,9 @@ namespace bbp { namespace sonata { namespace detail { class CompartmentLocation; -class CompartmentSet; class CompartmentSetFilteredIterator; +class CompartmentSet; +class CompartmentSets; } // namespace detail /** * CompartmentLocation public API. @@ -109,58 +110,54 @@ class SONATA_API CompartmentSet /// Serialize to JSON string std::string toJSON() const; + bool operator==(const CompartmentSet& other) const; + bool operator!=(const CompartmentSet& other) const; + private: std::shared_ptr impl_; }; +class SONATA_API CompartmentSets +{ +public: -// class SONATA_API CompartmentSet -// { -// public: + CompartmentSets(const std::string& content); + CompartmentSets(std::unique_ptr&& impl); + CompartmentSets(detail::CompartmentSets&& impl); + CompartmentSets(CompartmentSets&&) noexcept; + CompartmentSets(const CompartmentSets& other) = delete; + CompartmentSets& operator=(CompartmentSets&&) noexcept; + ~CompartmentSets(); + + static CompartmentSets fromFile(const std::string& path); + // Access element by key (throws if not found) + CompartmentSet at(const std::string& key) const; - -// std::size_t size() const; -// CompartmentLocation operator[](std::size_t index) const; -// const std::string& population() const; -// std::vector locations( -// const Selection& selection = Selection({})) const; -// Selection gids() const; -// std::string toJSON() const; - -// private: -// std::unique_ptr impl_; -// }; - -// class SONATA_API CompartmentSets -// { -// public: -// // Keep these exactly as-is: -// CompartmentSets(const std::string& content); -// CompartmentSets(std::unique_ptr&& impl); -// CompartmentSets(detail::CompartmentSets&& impl); -// CompartmentSets(CompartmentSets&&) noexcept; -// CompartmentSets(const CompartmentSets& other) = delete; -// CompartmentSets& operator=(CompartmentSets&&) noexcept; -// ~CompartmentSets(); - -// static CompartmentSets fromFile(const std::string& path); + // Number of compartment sets + std::size_t size() const; + + // Is empty? + bool empty() const; + + // Check if key exists + bool contains(const std::string& key) const; -// // Read-only dict-like API, Python-style names: -// size_t size() const; -// bool contains(const std::string& name) const; + // Get keys as set or vector (use vector here) + std::vector keys() const; -// std::vector keys() const; -// std::vector values() const; -// std::vector> items() const; + // Get all compartment sets as vector + std::vector values() const; -// CompartmentSet get(const std::string& name) const; + // Get items (key + compartment set) as vector of pairs + std::vector> items() const; -// std::string toJSON() const; + // Serialize all compartment sets to JSON string + std::string toJSON() const; -// private: -// std::unique_ptr impl_; -// }; +private: + std::unique_ptr impl_; +}; } // namespace sonata } // namespace bbp diff --git a/python/bindings.cpp b/python/bindings.cpp index 1e20c26..27176be 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -621,6 +621,12 @@ PYBIND11_MODULE(_libsonata, m) { return py::make_iterator(range.first, range.second); }, py::keep_alive<0, 1>()) + .def("__eq__", [](const CompartmentSet& self, const CompartmentSet& other) { + return self == other; + }) + .def("__ne__", [](const CompartmentSet& self, const CompartmentSet& other) { + return self != other; + }) .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)); // py::class_(m, "CompartmentSets", "CompartmentSets") diff --git a/python/tests/test_compartment_sets.py b/python/tests/test_compartment_sets.py index d9e4215..c4158ce 100644 --- a/python/tests/test_compartment_sets.py +++ b/python/tests/test_compartment_sets.py @@ -133,5 +133,24 @@ def test_toJSON_roundtrip(self): self.assertEqual(len(cs2), 4) self.assertEqual(cs2.population, self.cs.population) self.assertEqual([tuple(loc) for loc in cs2], [tuple(loc) for loc in self.cs]) + + def test_equality(self): + cs1 = CompartmentSet(self.json) + cs2 = CompartmentSet(self.json) + self.assertEqual(cs1, cs2) + self.assertFalse(cs1 != cs2) + + # Slightly modify JSON to create a different object + json_diff = '''{ + "population": "pop0", + "compartment_set": [ + [1, 10, 0.5], + [2, 20, 0.25], + [3, 30, 0.75] + ] + }''' + cs3 = CompartmentSet(json_diff) + self.assertNotEqual(cs1, cs3) + self.assertFalse(cs1 == cs3) diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 3c62008..196011f 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -297,6 +297,15 @@ class CompartmentSet { } return std::unique_ptr(new CompartmentSet(population_, std::move(filtered))); } + + bool operator==(const CompartmentSet& other) const { + return (population_ == other.population_) && + (compartment_locations_ == other.compartment_locations_); + } + + bool operator!=(const CompartmentSet& other) const { + return !(*this == other); + } }; class CompartmentSets { @@ -345,6 +354,10 @@ class CompartmentSets return data_.find(key) != data_.end(); } + bool empty() const { + return data_.empty(); + } + std::vector keys() const { std::vector result; result.reserve(data_.size()); @@ -516,117 +529,92 @@ CompartmentSet CompartmentSet::filter(const bbp::sonata::Selection& selection) c return CompartmentSet(impl_->filter(selection)); } +bool CompartmentSet::operator==(const CompartmentSet& other) const { + return *impl_ == *(other.impl_); +} + +bool CompartmentSet::operator!=(const CompartmentSet& other) const { + return *impl_ != *(other.impl_); +} std::string CompartmentSet::toJSON() const { return impl_->to_json().dump(); } +// CompartmentSets public API +CompartmentSets::CompartmentSets(const std::string& content) + : impl_(new detail::CompartmentSets(content)) {} +CompartmentSets::CompartmentSets(std::unique_ptr&& impl) + : impl_(std::move(impl)) {} +CompartmentSets::CompartmentSets(detail::CompartmentSets&& impl) + : CompartmentSets(std::make_unique(impl)) {} +CompartmentSets::CompartmentSets(CompartmentSets&&) noexcept = default; +CompartmentSets& CompartmentSets::operator=(CompartmentSets&&) noexcept = default; +CompartmentSets::~CompartmentSets() = default; -// CompartmentSet public API -// CompartmentSet::CompartmentSet(const std::string& content) -// : impl_(new detail::CompartmentSet(content)) {} -// CompartmentSet::CompartmentSet(std::unique_ptr&& impl) -// : impl_(std::move(impl)) {} -// CompartmentSet::CompartmentSet(detail::CompartmentSet&& impl) -// : CompartmentSet(std::make_unique(impl)) {} - -// CompartmentSet::CompartmentSet(CompartmentSet&&) noexcept = default; -// CompartmentSet& CompartmentSet::operator=(CompartmentSet&&) noexcept = default; -// CompartmentSet::~CompartmentSet() = default; - -// std::size_t CompartmentSet::size() const { -// return impl_->size(); -// } - -// CompartmentLocation CompartmentSet::operator[](std::size_t index) const { -// return (*impl_)[index]; -// } - -// const std::string& CompartmentSet::population() const { -// return impl_->population(); -// } - -// std::vector CompartmentSet::locations( -// const Selection& selection) const { -// std::vector view; -// auto raw_locs = impl_->locations(selection); -// view.reserve(raw_locs.size()); -// for (auto& el : raw_locs) { -// view.emplace_back(el); // take ownership -// } -// return view; -// } - -// Selection CompartmentSet::gids() const { -// return impl_->gids(); -// } - -// std::string CompartmentSet::toJSON() const { -// return impl_->to_json().dump(4); // Pretty print with 4 spaces -// } - -// // CompartmentSets public API - -// CompartmentSets::CompartmentSets(const std::string& content) -// : impl_(new detail::CompartmentSets(content)) {} -// CompartmentSets::CompartmentSets(std::unique_ptr&& impl) -// : impl_(std::move(impl)) {} -// CompartmentSets::CompartmentSets(detail::CompartmentSets&& impl) -// : CompartmentSets(std::make_unique(impl)) {} - -// CompartmentSets::CompartmentSets(CompartmentSets&&) noexcept = default; -// CompartmentSets& CompartmentSets::operator=(CompartmentSets&&) noexcept = default; -// CompartmentSets::~CompartmentSets() = default; - - - -// CompartmentSets CompartmentSets::fromFile(const std::string& path) { -// return detail::CompartmentSets::fromFile(path); -// } - -// size_t CompartmentSets::size() const { -// return impl_->size(); -// } - -// bool CompartmentSets::contains(const std::string& name) const { -// return impl_->contains(name); -// } - -// std::vector CompartmentSets::keys() const { -// return impl_->keys(); // Assuming detail::CompartmentSets has keys() returning std::set -// } - -// CompartmentSet CompartmentSets::get(const std::string& name) const { -// // Assuming impl_->getCompartmentSet returns detail::CompartmentSet by value or reference -// return impl_->get(name); -// } - -// std::vector CompartmentSets::values() const { -// std::vector result; -// result.reserve(size()); -// for (const auto& key : keys()) { -// result.emplace_back(impl_->get(key)); -// } -// return result; -// } - -// std::vector> CompartmentSets::items() const { -// std::vector> result; -// for (const auto& key : keys()) { -// result.emplace_back(key, impl_->get(key)); -// } -// return result; -// } - -// std::string CompartmentSets::toJSON() const { -// return impl_->to_json().dump(4); // Pretty print with 4 spaces -// } +CompartmentSets CompartmentSets::fromFile(const std::string& path) { + return detail::CompartmentSets::fromFile(path); +} +CompartmentSet CompartmentSets::at(const std::string& key) const { + return CompartmentSet(impl_->at(key)); +} +// Number of compartment sets +std::size_t CompartmentSets::size() const { + return impl_->size(); +} + +// is empty? +bool CompartmentSets::empty() const { + return impl_->empty(); +} + +// Check if key exists +bool CompartmentSets::contains(const std::string& key) const { + return impl_->contains(key); +} + +// Get keys as set or vector (use vector here) +std::vector CompartmentSets::keys() const { + return impl_->keys(); +} + +// Get all compartment sets as vector +std::vector CompartmentSets::values() const { + const auto vals = impl_->values(); + std::vector result; + result.reserve(vals.size()); + std::transform(vals.begin(), vals.end(), std::back_inserter(result), + [](std::shared_ptr ptr) { return CompartmentSet(std::move(ptr)); }); + + return result; +} + +// Get items (key + compartment set) as vector of pairs +std::vector> CompartmentSets::items() const { + auto items_vec = impl_->items(); + + std::vector> result; + result.reserve(items_vec.size()); + + std::transform( + items_vec.begin(), items_vec.end(), + std::back_inserter(result), + [](auto kv) { // pass by value to own the shared_ptr + return std::make_pair(std::move(kv.first), CompartmentSet(std::move(kv.second))); + }); + + return result; +} +// Serialize all compartment sets to JSON string +std::string CompartmentSets::toJSON() const { + return impl_->to_json().dump(); +} } // namespace sonata } // namespace bbp \ No newline at end of file diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index 3312f3b..112c45b 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -190,6 +190,61 @@ TEST_CASE("CompartmentSet public API") { auto no_filtered_gids = no_filtered.gids().flatten(); REQUIRE(no_filtered_gids == std::vector({1, 2, 3})); } + + SECTION("Equality and inequality operators") { + std::string json1 = R"( + { + "population": "pop1", + "compartment_set": [ + [1, 10, 0.5], + [2, 20, 0.25] + ] + } + )"; + + std::string json2 = R"( + { + "population": "pop1", + "compartment_set": [ + [1, 10, 0.5], + [2, 20, 0.25] + ] + } + )"; + + std::string json_different = R"( + { + "population": "pop1", + "compartment_set": [ + [1, 10, 0.5], + [2, 20, 0.3] + ] + } + )"; + + std::string json_different2 = R"( + { + "population": "pop2", + "compartment_set": [ + [1, 10, 0.5], + [2, 20, 0.25] + ] + } + )"; + + CompartmentSet cs1(json1); + CompartmentSet cs2(json2); + CompartmentSet cs3(json_different); + CompartmentSet cs4(json_different2); + + REQUIRE(cs1 == cs2); + REQUIRE_FALSE(cs1 != cs2); + + REQUIRE(cs1 != cs3); + REQUIRE_FALSE(cs1 == cs3); + REQUIRE_FALSE(cs1 == cs4); + } + } From 6b76b22168763f0d58768d5b1b945ec8ca644cf2 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 4 Jun 2025 17:19:45 +0200 Subject: [PATCH 21/44] done ctests --- include/bbp/sonata/compartment_sets.h | 6 + src/compartment_sets.cpp | 49 ++++ tests/test_compartment_sets.cpp | 311 +++++++++++++++----------- 3 files changed, 239 insertions(+), 127 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 64db3e6..2bb0d4d 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -97,6 +97,9 @@ class SONATA_API CompartmentSet /// Size of the set, optionally filtered by selection std::size_t size(const bbp::sonata::Selection& selection = bbp::sonata::Selection({})) const; + // Is empty? + bool empty() const; + /// Population name const std::string& population() const; @@ -155,6 +158,9 @@ class SONATA_API CompartmentSets // Serialize all compartment sets to JSON string std::string toJSON() const; + bool operator==(const CompartmentSets& other) const; + bool operator!=(const CompartmentSets& other) const; + private: std::unique_ptr impl_; }; diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 196011f..d02a7de 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -245,6 +245,10 @@ class CompartmentSet { })); } + std::size_t empty() const { + return compartment_locations_.empty(); + } + std::unique_ptr operator[](std::size_t index) const { return compartment_locations_.at(index).clone(); } @@ -388,6 +392,39 @@ class CompartmentSets } return j; } + + bool operator==(const CompartmentSets& other) { + if (data_.size() != other.data_.size()) { + return false; + } + + for (const auto& kv : data_) { + const auto& key = kv.first; + const auto& this_set = kv.second; + + auto it = other.data_.find(key); + if (it == other.data_.end()) { + return false; + } + + const auto& other_set = it->second; + if (this_set == nullptr && other_set == nullptr) { + continue; + } + if (this_set == nullptr || other_set == nullptr) { + return false; + } + if (*this_set != *other_set) { + return false; + } + } + + return true; + } + + bool operator!=(const CompartmentSets& other) { + return !(*this == other); + } }; } // namespace detail @@ -513,6 +550,10 @@ std::size_t CompartmentSet::size(const bbp::sonata::Selection& selection) const return impl_->size(selection); } +bool CompartmentSet::empty() const { + return impl_->empty(); +} + const std::string& CompartmentSet::population() const { return impl_->population(); } @@ -616,5 +657,13 @@ std::string CompartmentSets::toJSON() const { return impl_->to_json().dump(); } +bool CompartmentSets::operator==(const CompartmentSets& other) const { + return *impl_ == *(other.impl_); +} + +bool CompartmentSets::operator!=(const CompartmentSets& other) const { + return *impl_ != *(other.impl_); +} + } // namespace sonata } // namespace bbp \ No newline at end of file diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index 112c45b..d1ab818 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -242,137 +242,194 @@ TEST_CASE("CompartmentSet public API") { REQUIRE(cs1 != cs3); REQUIRE_FALSE(cs1 == cs3); - REQUIRE_FALSE(cs1 == cs4); + REQUIRE_FALSE(cs1 == cs4); } } +TEST_CASE("CompartmentSets public API") { + const std::string json = R"({ + "cs1": { + "population": "pop1", + "compartment_set": [ + [0, 10, 0.1], + [0, 10, 0.2], + [0, 10, 0.1], + [2, 3, 0.1], + [3, 6, 0.3] + ] + }, + "cs0": { + "population": "pop0", + "compartment_set": [] + } + })"; + + const auto cs1 = CompartmentSet(R"({ + "population": "pop1", + "compartment_set": [ + [0, 10, 0.1], + [0, 10, 0.2], + [0, 10, 0.1], + [2, 3, 0.1], + [3, 6, 0.3] + ] + })"); + + const auto cs0 = CompartmentSet(R"({ + "population": "pop0", + "compartment_set": [] + })"); + + SECTION("Load from file and basic properties") { + auto sets = CompartmentSets::fromFile("./data/compartment_sets.json"); + + CHECK(sets.size() == 2); + CHECK_FALSE(sets.empty()); + + auto keys = sets.keys(); + REQUIRE(sets.keys() == std::vector{"cs0", "cs1"}); + + CHECK(sets.contains("cs0")); + CHECK(sets.contains("cs1")); + + const auto& cs0 = sets.at("cs0"); + CHECK(cs0.empty()); -// TEST_CASE("CompartmentSets: fail on invalid JSON strings") { -// // Top level must be an object -// REQUIRE_THROWS_AS(CompartmentSets("1"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets("[\"array\"]"), SonataError); - -// // Each CompartmentSet must be an object with 'population' and 'compartment_set' keys -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": 1 })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": "string" })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": null })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": true })"), SonataError); - -// // Missing keys -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "compartment_set": [] } })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0" } })"), SonataError); - -// // Invalid types -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": 123, "compartment_set": [] } })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": null, "compartment_set": [] } })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": "not an array" } })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": 123 } })"), SonataError); - -// // Invalid compartment_set elements -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [1] } })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 2]] } })"), SonataError); - -// // Wrong types inside compartment_set elements -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [["not uint64", 0, 0.5]] } })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, "not uint64", 0.5]] } })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 0, "not a number"]] } })"), SonataError); - -// // Location out of bounds -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 0, -0.1]] } })"), SonataError); -// REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 0, 1.1]] } })"), SonataError); -// } - -// TEST_CASE("CompartmentSets: load from valid JSON file") { -// const auto sets = CompartmentSets::fromFile("./data/compartment_sets.json"); - -// REQUIRE(sets.size() == 2); - -// SECTION("Key presence") { -// REQUIRE(sets.contains("cs0")); -// REQUIRE(sets.contains("cs1")); -// } - -// SECTION("Key ordering") { -// const auto keys = sets.keys(); -// REQUIRE(keys == std::vector{"cs0", "cs1"}); -// } - -// SECTION("Values and items") { -// const auto values = sets.values(); -// REQUIRE(values.size() == 2); -// REQUIRE(values[0].population() == "pop0"); -// REQUIRE(values[1].population() == "pop1"); - -// const auto items = sets.items(); -// REQUIRE(items.size() == 2); -// REQUIRE(items[0].first == "cs0"); -// REQUIRE(items[0].second.population() == "pop0"); -// } - -// SECTION("Get by name") { -// const auto cs0 = sets.get("cs0"); -// REQUIRE(cs0.population() == "pop0"); -// } -// } - -// TEST_CASE("CompartmentSets: round-trip serialization") { -// std::string json = R"({ -// "cs0": { -// "population": "P", -// "compartment_set": [[1, 1, 0.1]] -// } -// })"; - -// CompartmentSets sets(json); -// std::string out = sets.toJSON(); -// CompartmentSets reloaded(out); - -// REQUIRE(reloaded.contains("cs0")); -// auto set = reloaded.get("cs0"); -// auto locs = set.locations(); -// REQUIRE(locs.size() == 1); -// REQUIRE(locs[0].gid() == 1); -// REQUIRE(locs[0].offset() == Approx(0.1)); -// } - - -// TEST_CASE("CompartmentLocation: construction and JSON round-trip") { -// CompartmentLocation loc(42, 3, 0.75); -// REQUIRE(loc.gid() == 42); -// REQUIRE(loc.sectionIdx() == 3); -// REQUIRE(loc.offset() == Approx(0.75)); - -// std::string json = loc.toJSON(); -// CompartmentLocation parsed(json); -// REQUIRE(parsed == loc); -// CompartmentLocation loc0("[42, 3, 0.75]"); -// REQUIRE(loc == loc0); -// } - -// TEST_CASE("CompartmentSet: valid JSON parsing and access") { -// std::string json = R"({ -// "population": "exc", -// "compartment_set": [ -// [1, 0, 0.0], -// [2, 1, 0.5], -// [1, 2, 1.0] -// ] -// })"; - -// CompartmentSet set(json); -// REQUIRE(set.population() == "exc"); - -// auto gids = set.gids(); -// REQUIRE(gids.flatten().size() == 2); -// REQUIRE(gids.contains(1)); -// REQUIRE(gids.contains(2)); - -// auto locs = set.getCompartmentLocations(); -// REQUIRE(locs.size() == 3); -// REQUIRE(locs[0].gid() == 1); -// REQUIRE(locs[1].sectionIdx() == 1); -// REQUIRE(locs[2].offset() == Approx(1.0)); -// } + const auto& cs1 = sets.at("cs1"); + CHECK_FALSE(cs1.empty()); + } + + SECTION("Equality operator from file and string") { + // Load from file + auto sets_from_file = CompartmentSets::fromFile("./data/compartment_sets.json"); + + // The JSON string as in the file + const std::string json = R"({ + "cs1": { + "population": "pop1", + "compartment_set": [ + [0, 10, 0.1], + [0, 10, 0.2], + [0, 10, 0.1], + [2, 3, 0.1], + [3, 6, 0.3] + ] + }, + "cs0": { + "population": "pop0", + "compartment_set": [] + } + })"; + + // Construct from JSON string directly + CompartmentSets sets_from_string(json); + + // They should be equal + CHECK(sets_from_file == sets_from_string); + CHECK_FALSE(sets_from_file != sets_from_string); + + // Now change the string slightly and check inequality + const std::string json_modified = R"({ + "cs1": { + "population": "pop1", + "compartment_set": [ + [0, 10, 0.8], + [0, 10, 0.2], + [0, 10, 0.1], + [2, 3, 0.1], + [3, 6, 0.3] + ] + }, + "cs0": { + "population": "pop0", + "compartment_set": [] + } + })"; + + CompartmentSets sets_modified(json_modified); + + CHECK(sets_from_file != sets_modified); + CHECK_FALSE(sets_from_file == sets_modified); + } + + SECTION("Throws on missing key") { + auto sets = CompartmentSets::fromFile("./data/compartment_sets.json"); + CHECK_THROWS_AS(sets.at("not_there"), std::out_of_range); + } + + SECTION("JSON serialization round-trip") { + auto sets = CompartmentSets::fromFile("./data/compartment_sets.json"); + + auto json = sets.toJSON(); + CompartmentSets roundtrip(json); + + CHECK(sets == roundtrip); + } + + SECTION("Keys returns correct vector") { + CompartmentSets sets(json); + auto keys = sets.keys(); + CHECK(keys == std::vector{"cs0", "cs1"}); + } + + SECTION("Values returns vector of CompartmentSet") { + CompartmentSets sets(json); + CHECK(sets.values() == std::vector{cs0, cs1}); + } + + SECTION("Items returns vector of pairs (key, CompartmentSet)") { + CompartmentSets sets(json); + CHECK(sets.items() == std::vector>{{"cs0", cs0}, {"cs1", cs1}}); + } + + SECTION("Size method") { + CompartmentSets sets(json); + CHECK(sets.size() == 2); + } + + SECTION("Contains method") { + CompartmentSets sets(json); + CHECK(sets.contains("cs0")); + CHECK(sets.contains("cs1")); + CHECK_FALSE(sets.contains("missing_key")); + } + + SECTION("Invalid JSON parsings") { + // Top level must be an object + REQUIRE_THROWS_AS(CompartmentSets("1"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets("[\"array\"]"), SonataError); + + // Each CompartmentSet must be an object with 'population' and 'compartment_set' keys + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": 1 })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": "string" })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": null })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": true })"), SonataError); + + // Missing keys + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "compartment_set": [] } })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0" } })"), SonataError); + + // Invalid types + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": 123, "compartment_set": [] } })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": null, "compartment_set": [] } })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": "not an array" } })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": 123 } })"), SonataError); + + // Invalid compartment_set elements + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [1] } })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 2]] } })"), SonataError); + + // Wrong types inside compartment_set elements + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [["not uint64", 0, 0.5]] } })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, "not uint64", 0.5]] } })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 0, "not a number"]] } })"), SonataError); + + // Location out of bounds + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 0, -0.1]] } })"), SonataError); + REQUIRE_THROWS_AS(CompartmentSets(R"({ "cs0": { "population": "pop0", "compartment_set": [[1, 0, 1.1]] } })"), SonataError); + } + +} From 64c0622e1db5adcdb60802fdae43634d1c6a1648 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 4 Jun 2025 18:03:52 +0200 Subject: [PATCH 22/44] done with pytests. no format --- python/bindings.cpp | 65 ++++++++++++++++------- python/generated/docstrings.h | 4 ++ python/libsonata/__init__.py | 2 + python/tests/test_compartment_sets.py | 76 +++++++++++++++++++++++++++ test.py | 36 +++++++++++-- 5 files changed, 160 insertions(+), 23 deletions(-) diff --git a/python/bindings.cpp b/python/bindings.cpp index 27176be..9ed034d 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -627,24 +627,53 @@ PYBIND11_MODULE(_libsonata, m) { .def("__ne__", [](const CompartmentSet& self, const CompartmentSet& other) { return self != other; }) - .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)); - - // py::class_(m, "CompartmentSets", "CompartmentSets") - // .def(py::init()) - // .def_static( - // "from_file", - // [](py::object path) { - // return CompartmentSets::fromFile(py::str(path)); - // }, - // py::arg("path"), - // "Create a CompartmentSets object from a file") - // .def("__contains__", &CompartmentSets::contains) - // .def("__len__", &CompartmentSets::size) - // .def("__getitem__", &CompartmentSets::get, py::arg("name")) - // .def("keys", &CompartmentSets::keys, DOC_COMPARTMENTSETS(keys)) - // .def("values", &CompartmentSets::values, DOC_COMPARTMENTSETS(values)) - // .def("items", &CompartmentSets::items, DOC_COMPARTMENTSETS(items)) - // .def("toJSON", &CompartmentSets::toJSON, DOC_COMPARTMENTSETS(toJSON)); + .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)) + .def("__repr__", + [](const CompartmentSet& self) { + auto range = self.filtered_crange(bbp::sonata::Selection({})); + std::vector parts; + for (auto it = range.first; it != range.second; ++it) { + parts.push_back(py::repr(py::cast(*it))); + } + auto joined = py::str(", ").attr("join")(parts); + return "CompartmentSet(population=" + py::repr(py::cast(self.population())).cast() + + ", compartments=[" + joined.cast() + "])"; + }) + .def("__str__", + [](const CompartmentSet& self) { + return py::str(py::repr(py::cast(self))); + }); + + py::class_(m, "CompartmentSets") + .def(py::init()) + .def_static("fromFile", &CompartmentSets::fromFile, py::arg("path")) + .def("at", &CompartmentSets::at, py::arg("key")) + .def("__contains__", &CompartmentSets::contains, py::arg("key"), DOC_COMPARTMENTSETS(contains)) + .def("keys", &CompartmentSets::keys) + .def("values", &CompartmentSets::values) + .def("items", &CompartmentSets::items) + .def("toJSON", &CompartmentSets::toJSON, DOC_COMPARTMENTSETS(toJSON)) + .def("__eq__", &CompartmentSets::operator==) + .def("__ne__", &CompartmentSets::operator!=) + .def("__len__", &CompartmentSets::size) + .def("__getitem__", &CompartmentSets::at, py::arg("key"), DOC_COMPARTMENTSET(getitem)) + .def("__repr__", + [](const CompartmentSets& self) { + auto items = self.items(); + std::vector parts; + for (const auto& item : items) { + // Build "key: value" strings + auto key_repr = py::repr(py::cast(item.first)); + auto val_repr = py::repr(py::cast(item.second)); + parts.push_back(key_repr.attr("__str__")() + py::str(": ") + val_repr.attr("__str__")()); + } + auto joined = py::str(", ").attr("join")(parts); + return "CompartmentSets({" + joined.cast() + "})"; + }) + .def("__str__", + [](const CompartmentSets& self) { + return py::str(py::repr(py::cast(self))); + }); py::class_(m, "CommonPopulationProperties", diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 8095b38..c27c965 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -409,6 +409,10 @@ static const char *__doc_bbp_sonata_CompartmentSets_values = R"doc(Return the va static const char *__doc_bbp_sonata_CompartmentSets_items = R"doc(Return the (key, value) pairs of the CompartmentSets)doc"; +static const char *__doc_bbp_sonata_CompartmentSets_contains = R"doc(Check if key exists.)doc"; + +static const char *__doc_bbp_sonata_CompartmentSets_getitem = R"doc("Get a CompartmentSet by key.")doc"; + static const char *__doc_bbp_sonata_CompartmentSets_toJSON = R"doc(Serialize CompartmentSets to a JSON string)doc"; static const char *__doc_bbp_sonata_NodeSets_update = diff --git a/python/libsonata/__init__.py b/python/libsonata/__init__.py index cca83aa..93fce0a 100644 --- a/python/libsonata/__init__.py +++ b/python/libsonata/__init__.py @@ -16,6 +16,7 @@ NodeSets, CompartmentLocation, CompartmentSet, + CompartmentSets, NodeStorage, Selection, SomaDataFrame, @@ -42,6 +43,7 @@ "NodeSets", "CompartmentLocation", "CompartmentSet", + "CompartmentSets", "NodeStorage", "Selection", "SomaDataFrame", diff --git a/python/tests/test_compartment_sets.py b/python/tests/test_compartment_sets.py index c4158ce..e038137 100644 --- a/python/tests/test_compartment_sets.py +++ b/python/tests/test_compartment_sets.py @@ -5,6 +5,7 @@ from libsonata import ( CompartmentLocation, CompartmentSet, + CompartmentSets, Selection ) @@ -153,4 +154,79 @@ def test_equality(self): self.assertNotEqual(cs1, cs3) self.assertFalse(cs1 == cs3) + def test_repr_and_str(self): + r = repr(self.cs) + s = str(self.cs) + print(r) + self.assertTrue(r.startswith("CompartmentSet(population")) + self.assertEqual(s, r) + +class TestCompartmentSets(unittest.TestCase): + def setUp(self): + # Load valid json string from file + with open(os.path.join(PATH, 'compartment_sets.json'), 'r') as f: + self.json_str = f.read() + self.cs = CompartmentSets(self.json_str) + + def test_init_from_string(self): + self.assertIsInstance(self.cs, CompartmentSets) + self.assertGreater(len(self.cs), 0) + + def test_contains(self): + keys = self.cs.keys() + for key in keys: + self.assertIn(key, self.cs) + self.assertNotIn('non_existing_key', self.cs) + + def test_getitem(self): + keys = self.cs.keys() + if keys: + key = keys[0] + val = self.cs[key] + self.assertIsInstance(val, CompartmentSet) + + def test_keys_values_items(self): + keys = self.cs.keys() + values = self.cs.values() + items = self.cs.items() + self.assertEqual(len(keys), len(values)) + self.assertEqual(len(keys), len(items)) + for k, v in items: + self.assertIn(k, keys) + self.assertIn(v, values) + + def test_equality(self): + cs1 = CompartmentSets(self.json_str) + cs2 = CompartmentSets(self.json_str) + self.assertEqual(cs1, cs2) + self.assertFalse(cs1 != cs2) + + # Modify JSON to create different object + altered = json.loads(self.json_str) + if altered: + # Remove one key if possible + some_key = list(altered.keys())[0] + altered.pop(some_key) + altered_json = json.dumps(altered) + cs3 = CompartmentSets(altered_json) + self.assertNotEqual(cs1, cs3) + self.assertFalse(cs1 == cs3) + + def test_toJSON_roundtrip(self): + json_out = self.cs.toJSON() + cs2 = CompartmentSets(json_out) + self.assertEqual(self.cs, cs2) + + def test_static_fromFile(self): + cs_file = CompartmentSets.fromFile(os.path.join(PATH, 'compartment_sets.json')) + self.assertEqual(cs_file, self.cs) + + def test_repr_and_str(self): + r = repr(self.cs) + s = str(self.cs) + self.assertTrue(r.startswith("CompartmentSets({")) + self.assertEqual(s, r) # str delegates to repr + # repr should contain keys from the dict + for key in self.cs.keys(): + self.assertIn(str(key), r) \ No newline at end of file diff --git a/test.py b/test.py index baf9884..f9ec45d 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,7 @@ from libsonata import ( CompartmentLocation, + CompartmentSet, + CompartmentSets ) def inspect(v): @@ -8,11 +10,35 @@ def inspect(v): # if not i.startswith('__'): print(f' {i} = {getattr(v, i)}') + +json = '''{ + "population": "pop0", + "compartment_set": [ + [1, 10, 0.5], + [2, 20, 0.25], + [3, 30, 0.75], + [2, 20, 0.25] + ] + }''' +cs = CompartmentSet(json) +print(cs) + # a = CompartmentSets('{ "CompartmentSet0": { "population": "pop0", "compartment_set": [[0, 10, 0.2], [3, 11, 0.2], [0, 10, 0.201], [1, 11, 0.2]] }, "CompartmentSet1": { "population": "pop1", "compartment_set": [[0, 10, 0.2], [3, 11, 0.2], [0, 10, 0.201], [1, 11, 0.2]] } }') -b = CompartmentLocation(1, 2, 0.3) -a = CompartmentLocation(4, 5, 0.6) -b = a +# b = CompartmentLocation(1, 2, 0.3) +# a = CompartmentLocation(4, 5, 0.6) +# b = a + +# print(id(b), b) -print(id(b), b) +# print(id(a), a) -print(id(a), a) +# a = CompartmentSet('''{ +# "population": "pop0", +# "compartment_set": [ +# [1, 10, 0.5], +# [2, 20, 0.25], +# [3, 30, 0.75], +# [2, 20, 0.25] +# ] +# }''') +print(a) From 02295b54ef223d61326eaba6ddff8e6d1c09c39e Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 4 Jun 2025 18:05:40 +0200 Subject: [PATCH 23/44] format? --- python/bindings.cpp | 117 +++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/python/bindings.cpp b/python/bindings.cpp index 9ed034d..8b8906e 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -582,19 +582,23 @@ PYBIND11_MODULE(_libsonata, m) { [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); }); - + py::class_(m, "CompartmentSet") .def(py::init()) - .def_property_readonly("population", &CompartmentSet::population, DOC_COMPARTMENTSET(population)) + .def_property_readonly("population", + &CompartmentSet::population, + DOC_COMPARTMENTSET(population)) .def("size", - py::overload_cast(&CompartmentSet::size, py::const_), - py::arg("selection") = bbp::sonata::Selection({}), DOC_COMPARTMENTSET(size)) + py::overload_cast(&CompartmentSet::size, py::const_), + py::arg("selection") = bbp::sonata::Selection({}), + DOC_COMPARTMENTSET(size)) .def("gids", &CompartmentSet::gids, DOC_COMPARTMENTSET(gids)) .def("filter", - &CompartmentSet::filter, - py::arg("selection") = bbp::sonata::Selection({}), - DOC_COMPARTMENTSET(filter)) - .def("filtered_iter", + &CompartmentSet::filter, + py::arg("selection") = bbp::sonata::Selection({}), + DOC_COMPARTMENTSET(filter)) + .def( + "filtered_iter", [](const CompartmentSet& self, const bbp::sonata::Selection& sel) { auto range = self.filtered_crange(sel); return py::make_iterator(range.first, range.second); @@ -602,53 +606,55 @@ PYBIND11_MODULE(_libsonata, m) { py::arg("selection") = bbp::sonata::Selection({}), py::keep_alive<0, 1>(), DOC_COMPARTMENTSET(filteredIter)) - .def("__len__", [](const CompartmentSet& self) { - return self.size(); - }) - .def("__getitem__", [](const CompartmentSet& self, py::ssize_t i) { - if (i < 0) { - i += static_cast(self.size()); - } - if (i < 0 || static_cast(i) >= self.size()) { - throw py::index_error("Index out of range"); - } - return self[static_cast(i)]; - }, py::arg("index"), + .def("__len__", [](const CompartmentSet& self) { return self.size(); }) + .def( + "__getitem__", + [](const CompartmentSet& self, py::ssize_t i) { + if (i < 0) { + i += static_cast(self.size()); + } + if (i < 0 || static_cast(i) >= self.size()) { + throw py::index_error("Index out of range"); + } + return self[static_cast(i)]; + }, + py::arg("index"), DOC_COMPARTMENTSET(getitem)) - .def("__iter__", + .def( + "__iter__", [](const CompartmentSet& self) { auto range = self.filtered_crange(bbp::sonata::Selection({})); return py::make_iterator(range.first, range.second); }, py::keep_alive<0, 1>()) - .def("__eq__", [](const CompartmentSet& self, const CompartmentSet& other) { - return self == other; - }) - .def("__ne__", [](const CompartmentSet& self, const CompartmentSet& other) { - return self != other; - }) + .def("__eq__", + [](const CompartmentSet& self, const CompartmentSet& other) { return self == other; }) + .def("__ne__", + [](const CompartmentSet& self, const CompartmentSet& other) { return self != other; }) .def("toJSON", &CompartmentSet::toJSON, DOC_COMPARTMENTSET(toJSON)) .def("__repr__", - [](const CompartmentSet& self) { - auto range = self.filtered_crange(bbp::sonata::Selection({})); - std::vector parts; - for (auto it = range.first; it != range.second; ++it) { - parts.push_back(py::repr(py::cast(*it))); - } - auto joined = py::str(", ").attr("join")(parts); - return "CompartmentSet(population=" + py::repr(py::cast(self.population())).cast() + - ", compartments=[" + joined.cast() + "])"; - }) + [](const CompartmentSet& self) { + auto range = self.filtered_crange(bbp::sonata::Selection({})); + std::vector parts; + for (auto it = range.first; it != range.second; ++it) { + parts.push_back(py::repr(py::cast(*it))); + } + auto joined = py::str(", ").attr("join")(parts); + return "CompartmentSet(population=" + + py::repr(py::cast(self.population())).cast() + + ", compartments=[" + joined.cast() + "])"; + }) .def("__str__", - [](const CompartmentSet& self) { - return py::str(py::repr(py::cast(self))); - }); + [](const CompartmentSet& self) { return py::str(py::repr(py::cast(self))); }); py::class_(m, "CompartmentSets") .def(py::init()) .def_static("fromFile", &CompartmentSets::fromFile, py::arg("path")) .def("at", &CompartmentSets::at, py::arg("key")) - .def("__contains__", &CompartmentSets::contains, py::arg("key"), DOC_COMPARTMENTSETS(contains)) + .def("__contains__", + &CompartmentSets::contains, + py::arg("key"), + DOC_COMPARTMENTSETS(contains)) .def("keys", &CompartmentSets::keys) .def("values", &CompartmentSets::values) .def("items", &CompartmentSets::items) @@ -658,22 +664,21 @@ PYBIND11_MODULE(_libsonata, m) { .def("__len__", &CompartmentSets::size) .def("__getitem__", &CompartmentSets::at, py::arg("key"), DOC_COMPARTMENTSET(getitem)) .def("__repr__", - [](const CompartmentSets& self) { - auto items = self.items(); - std::vector parts; - for (const auto& item : items) { - // Build "key: value" strings - auto key_repr = py::repr(py::cast(item.first)); - auto val_repr = py::repr(py::cast(item.second)); - parts.push_back(key_repr.attr("__str__")() + py::str(": ") + val_repr.attr("__str__")()); - } - auto joined = py::str(", ").attr("join")(parts); - return "CompartmentSets({" + joined.cast() + "})"; - }) + [](const CompartmentSets& self) { + auto items = self.items(); + std::vector parts; + for (const auto& item : items) { + // Build "key: value" strings + auto key_repr = py::repr(py::cast(item.first)); + auto val_repr = py::repr(py::cast(item.second)); + parts.push_back(key_repr.attr("__str__")() + py::str(": ") + + val_repr.attr("__str__")()); + } + auto joined = py::str(", ").attr("join")(parts); + return "CompartmentSets({" + joined.cast() + "})"; + }) .def("__str__", - [](const CompartmentSets& self) { - return py::str(py::repr(py::cast(self))); - }); + [](const CompartmentSets& self) { return py::str(py::repr(py::cast(self))); }); py::class_(m, "CommonPopulationProperties", From 0bbaa3a7fac0ee7dfad6869e110be9afff2c16e7 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Wed, 4 Jun 2025 21:06:49 +0200 Subject: [PATCH 24/44] remove test --- test.py | 44 -------------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index f9ec45d..0000000 --- a/test.py +++ /dev/null @@ -1,44 +0,0 @@ -from libsonata import ( - CompartmentLocation, - CompartmentSet, - CompartmentSets -) - -def inspect(v): - print(v, type(v)) - for i in dir(v): - # if not i.startswith('__'): - print(f' {i} = {getattr(v, i)}') - - -json = '''{ - "population": "pop0", - "compartment_set": [ - [1, 10, 0.5], - [2, 20, 0.25], - [3, 30, 0.75], - [2, 20, 0.25] - ] - }''' -cs = CompartmentSet(json) -print(cs) - -# a = CompartmentSets('{ "CompartmentSet0": { "population": "pop0", "compartment_set": [[0, 10, 0.2], [3, 11, 0.2], [0, 10, 0.201], [1, 11, 0.2]] }, "CompartmentSet1": { "population": "pop1", "compartment_set": [[0, 10, 0.2], [3, 11, 0.2], [0, 10, 0.201], [1, 11, 0.2]] } }') -# b = CompartmentLocation(1, 2, 0.3) -# a = CompartmentLocation(4, 5, 0.6) -# b = a - -# print(id(b), b) - -# print(id(a), a) - -# a = CompartmentSet('''{ -# "population": "pop0", -# "compartment_set": [ -# [1, 10, 0.5], -# [2, 20, 0.25], -# [3, 30, 0.75], -# [2, 20, 0.25] -# ] -# }''') -print(a) From 8f3c6cee0444eada8ec4357271ea27b3e1b9051c Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 10:08:23 +0200 Subject: [PATCH 25/44] some fixes from Mike --- include/bbp/sonata/compartment_sets.h | 38 ++++++++++++++++++--------- include/bbp/sonata/selection.h | 2 +- python/bindings.cpp | 12 ++++----- python/generated/docstrings.h | 2 +- python/tests/test_compartment_sets.py | 12 ++++----- src/compartment_sets.cpp | 34 ++++++++++++------------ tests/test_compartment_sets.cpp | 8 +++--- 7 files changed, 60 insertions(+), 48 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 2bb0d4d..4b45cc0 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -13,11 +13,11 @@ class CompartmentSets; /** * CompartmentLocation public API. * - * This class uniquely identifies a compartment by a set of gid, section_idx and offset: + * This class uniquely identifies a compartment by a set of gid, section_index and offset: * * - gid: Global ID of the cell (Neuron) to which the compartment belongs. No * overlaps among populations. - * - section_idx: Absolute section index. Progressive index that uniquely identifies the section. + * - section_index: Absolute section index. Progressive index that uniquely identifies the section. * There is a mapping between neuron section names (i.e. dend[10]) and this index. * - offset: Offset of the compartment along the section. The offset is a value between 0 and 1 */ @@ -25,7 +25,7 @@ class SONATA_API CompartmentLocation { public: CompartmentLocation(); - CompartmentLocation(const int64_t gid, const int64_t section_idx, const double offset); + CompartmentLocation(const int64_t gid, const int64_t section_index, const double offset); explicit CompartmentLocation(const std::string& content); explicit CompartmentLocation(std::unique_ptr&& impl); CompartmentLocation(const CompartmentLocation& other); @@ -38,7 +38,7 @@ class SONATA_API CompartmentLocation bool operator!=(const CompartmentLocation& other) const noexcept; uint64_t gid() const; - uint64_t sectionIdx() const; + uint64_t sectionIndex() const; double offset() const; std::string toJSON() const; @@ -79,7 +79,7 @@ class SONATA_API CompartmentSetFilteredIterator { * CompartmentSet public API. * * This class represents a set of compartment locations associated with a neuron population. - * Each compartment is uniquely defined by a (gid, section_idx, offset) triplet. + * Each compartment is uniquely defined by a (gid, section_index, offset) triplet. * This API supports filtering based on a gid selection. */ class SONATA_API CompartmentSet @@ -120,6 +120,16 @@ class SONATA_API CompartmentSet std::shared_ptr impl_; }; +/** + * @class CompartmentSets + * @brief A container class that manages a collection of named CompartmentSet objects. + * + * This class provides methods for accessing, querying, and serializing a collection of + * compartment sets identified by string keys. It supports construction from a JSON string + * or a file, and encapsulates its internal implementation using the PIMPL idiom. + * + * The class is non-copyable but movable, and offers value-style accessors for ease of use. + */ class SONATA_API CompartmentSets { public: @@ -132,30 +142,32 @@ class SONATA_API CompartmentSets CompartmentSets& operator=(CompartmentSets&&) noexcept; ~CompartmentSets(); + /// Create new CompartmentSets from file. In this way we distinguish from + /// the basic string constructor. static CompartmentSets fromFile(const std::string& path); - // Access element by key (throws if not found) + /// Access element by key (throws if not found) CompartmentSet at(const std::string& key) const; - // Number of compartment sets + /// Number of compartment sets std::size_t size() const; - // Is empty? + /// Is empty? bool empty() const; - // Check if key exists + /// Check if key exists bool contains(const std::string& key) const; - // Get keys as set or vector (use vector here) + /// Get keys as a vector (use vector here) std::vector keys() const; - // Get all compartment sets as vector + /// Get all compartment sets as vector std::vector values() const; - // Get items (key + compartment set) as vector of pairs + /// Get items (key + compartment set) as vector of pairs std::vector> items() const; - // Serialize all compartment sets to JSON string + /// Serialize all compartment sets to JSON string std::string toJSON() const; bool operator==(const CompartmentSets& other) const; diff --git a/include/bbp/sonata/selection.h b/include/bbp/sonata/selection.h index 5886a9f..7deb156 100644 --- a/include/bbp/sonata/selection.h +++ b/include/bbp/sonata/selection.h @@ -45,7 +45,7 @@ class SONATA_API Selection bool empty() const; /** - * Check if Selection contains a given GID (binary search) + * Check if Selection contains a given GID * @param gid is the GID to check * @return true if Selection contains gid, false otherwise */ diff --git a/python/bindings.cpp b/python/bindings.cpp index 8b8906e..59439d5 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -551,18 +551,18 @@ PYBIND11_MODULE(_libsonata, m) { py::class_(m, "CompartmentLocation") .def(py::init()) .def(py::init(), - py::arg("gid"), py::arg("section_idx"), py::arg("offset")) + py::arg("gid"), py::arg("section_index"), py::arg("offset")) .def_property_readonly("gid", &CompartmentLocation::gid, DOC_COMPARTMENTLOCATION(gid)) - .def_property_readonly("section_idx", - &CompartmentLocation::sectionIdx, - DOC_COMPARTMENTLOCATION(sectionIdx)) + .def_property_readonly("section_index", + &CompartmentLocation::sectionIndex, + DOC_COMPARTMENTLOCATION(sectionIndex)) .def_property_readonly("offset", &CompartmentLocation::offset, DOC_COMPARTMENTLOCATION(offset)) .def("toJSON", &CompartmentLocation::toJSON, DOC_COMPARTMENTLOCATION(toJSON)) .def("__iter__", [](const CompartmentLocation& self) { - return py::iter(py::make_tuple(self.gid(), self.sectionIdx(), self.offset())); + return py::iter(py::make_tuple(self.gid(), self.sectionIndex(), self.offset())); }) .def("__eq__", &CompartmentLocation::operator==) .def("__ne__", &CompartmentLocation::operator!=) @@ -576,7 +576,7 @@ PYBIND11_MODULE(_libsonata, m) { .def("__repr__", [](const CompartmentLocation& self) { return py::str("CompartmentLocation({}, {}, {})") - .format(self.gid(), self.sectionIdx(), self.offset()); + .format(self.gid(), self.sectionIndex(), self.offset()); }) .def("__str__", [](const CompartmentLocation& self) { diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index c27c965..18cdbed 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -383,7 +383,7 @@ static const char *__doc_bbp_sonata_NodeSets_toJSON = R"doc(Return the nodesets static const char *__doc_bbp_sonata_CompartmentLocation_gid = R"doc(GID)doc"; -static const char *__doc_bbp_sonata_CompartmentLocation_sectionIdx = R"doc(Absolute section index. Progressive index that uniquely identifies the section. There is a mapping between neuron section names (i.e. dend[10]) and this index.)doc"; +static const char *__doc_bbp_sonata_CompartmentLocation_sectionIndex = R"doc(Absolute section index. Progressive index that uniquely identifies the section. There is a mapping between neuron section names (i.e. dend[10]) and this index.)doc"; static const char *__doc_bbp_sonata_CompartmentLocation_offset = R"doc(Offset of the compartment along the section)doc"; diff --git a/python/tests/test_compartment_sets.py b/python/tests/test_compartment_sets.py index e038137..28b7c3a 100644 --- a/python/tests/test_compartment_sets.py +++ b/python/tests/test_compartment_sets.py @@ -16,13 +16,13 @@ class TestCompartmentLocation(unittest.TestCase): def test_constructor_from_values(self): loc = CompartmentLocation(4, 40, 0.9) self.assertEqual(loc.gid, 4) - self.assertEqual(loc.section_idx, 40) + self.assertEqual(loc.section_index, 40) self.assertAlmostEqual(loc.offset, 0.9) def test_constructor_from_string(self): loc = CompartmentLocation("[4, 40, 0.9]") self.assertEqual(loc.gid, 4) - self.assertEqual(loc.section_idx, 40) + self.assertEqual(loc.section_index, 40) self.assertAlmostEqual(loc.offset, 0.9) def test_toJSON(self): @@ -44,9 +44,9 @@ def test_repr_and_str(self): def test_iterable(self): loc = CompartmentLocation(4, 40, 0.9) - gid, section_idx, offset = loc + gid, section_index, offset = loc self.assertEqual(gid, 4) - self.assertEqual(section_idx, 40) + self.assertEqual(section_index, 40) self.assertAlmostEqual(offset, 0.9) def test_assignment_creates_copy(self): @@ -98,11 +98,11 @@ def test_len_dunder(self): def test_getitem(self): loc = self.cs[0] - self.assertEqual((loc.gid, loc.section_idx, loc.offset), (1, 10, 0.5)) + self.assertEqual((loc.gid, loc.section_index, loc.offset), (1, 10, 0.5)) def test_getitem_negative_index(self): loc = self.cs[-1] - self.assertEqual((loc.gid, loc.section_idx, loc.offset), (2, 20, 0.25)) + self.assertEqual((loc.gid, loc.section_index, loc.offset), (2, 20, 0.25)) def test_getitem_out_of_bounds_raises(self): with self.assertRaises(IndexError): diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index d02a7de..da93108 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -21,7 +21,7 @@ class CompartmentLocation { private: std::uint64_t gid_; - std::uint64_t section_idx_; + std::uint64_t section_index_; double offset_; void setGid(int64_t gid) { @@ -30,12 +30,12 @@ class CompartmentLocation } gid_ = static_cast(gid); } - void setSectionIdx(int64_t section_idx) { - if (section_idx < 0) { + void setSectionIndex(int64_t section_index) { + if (section_index < 0) { throw SonataError( - fmt::format("Section index must be non-negative, got {}", section_idx)); + fmt::format("Section index must be non-negative, got {}", section_index)); } - section_idx_ = static_cast(section_idx); + section_index_ = static_cast(section_index); } void setOffset(double offset) { if (offset < 0.0 || offset > 1.0) { @@ -52,9 +52,9 @@ class CompartmentLocation CompartmentLocation(const CompartmentLocation& other) = default; CompartmentLocation(CompartmentLocation&&) noexcept = default; CompartmentLocation& operator=(CompartmentLocation&&) noexcept = default; - CompartmentLocation(int64_t gid, int64_t section_idx, double offset) { + CompartmentLocation(int64_t gid, int64_t section_index, double offset) { setGid(gid); - setSectionIdx(section_idx); + setSectionIndex(section_index); setOffset(offset); } @@ -64,12 +64,12 @@ class CompartmentLocation CompartmentLocation(const nlohmann::json& j) { if (!j.is_array() || j.size() != 3) { throw SonataError( - "CompartmentLocation must be an array of exactly 3 elements: [gid, section_idx, " + "CompartmentLocation must be an array of exactly 3 elements: [gid, section_index, " "offset]"); } setGid(get_int64_or_throw(j[0])); - setSectionIdx(get_int64_or_throw(j[1])); + setSectionIndex(get_int64_or_throw(j[1])); if (!j[2].is_number()) { throw SonataError("Fourth element (offset) must be a number"); @@ -81,8 +81,8 @@ class CompartmentLocation return gid_; } - uint64_t sectionIdx() const { - return section_idx_; + uint64_t sectionIndex() const { + return section_index_; } double offset() const { @@ -90,11 +90,11 @@ class CompartmentLocation } nlohmann::json to_json() const { - return nlohmann::json::array({gid_, section_idx_, offset_}); + return nlohmann::json::array({gid_, section_index_, offset_}); } bool operator==(const CompartmentLocation& other) const { - return gid_ == other.gid_ && section_idx_ == other.section_idx_ && + return gid_ == other.gid_ && section_index_ == other.section_index_ && std::abs(offset_ - other.offset_) < offsetTolerance; } bool operator!=(const CompartmentLocation& other) const { @@ -432,9 +432,9 @@ class CompartmentSets // CompartmentLocation public API CompartmentLocation::CompartmentLocation() = default; CompartmentLocation::CompartmentLocation(const int64_t gid, - const int64_t section_idx, + const int64_t section_index, const double offset) - : impl_(new detail::CompartmentLocation(gid, section_idx, offset)) {} + : impl_(new detail::CompartmentLocation(gid, section_index, offset)) {} CompartmentLocation::CompartmentLocation(const std::string& content) : impl_(new detail::CompartmentLocation(content)) {} CompartmentLocation::CompartmentLocation(std::unique_ptr&& impl) @@ -464,8 +464,8 @@ uint64_t CompartmentLocation::gid() const { return impl_->gid(); } -uint64_t CompartmentLocation::sectionIdx() const { - return impl_->sectionIdx(); +uint64_t CompartmentLocation::sectionIndex() const { + return impl_->sectionIndex(); } double CompartmentLocation::offset() const { diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index d1ab818..afc09cd 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -12,7 +12,7 @@ TEST_CASE("CompartmentLocation public API") { SECTION("Construct from valid gid, section_idx, offset") { CompartmentLocation loc(1, 10, 0.5); REQUIRE(loc.gid() == 1); - REQUIRE(loc.sectionIdx() == 10); + REQUIRE(loc.sectionIndex() == 10); REQUIRE(loc.offset() == Approx(0.5)); } @@ -20,7 +20,7 @@ TEST_CASE("CompartmentLocation public API") { std::string json_str = "[1, 10, 0.5]"; CompartmentLocation loc(json_str); REQUIRE(loc.gid() == 1); - REQUIRE(loc.sectionIdx() == 10); + REQUIRE(loc.sectionIndex() == 10); REQUIRE(loc.offset() == Approx(0.5)); } @@ -64,14 +64,14 @@ TEST_CASE("CompartmentLocation public API") { CompartmentLocation moved_constructed(std::move(original)); REQUIRE(moved_constructed.gid() == 3); - REQUIRE(moved_constructed.sectionIdx() == 30); + REQUIRE(moved_constructed.sectionIndex() == 30); REQUIRE(moved_constructed.offset() == Approx(0.8)); CompartmentLocation another(0, 0, 0); another = std::move(moved_constructed); REQUIRE(another.gid() == 3); - REQUIRE(another.sectionIdx() == 30); + REQUIRE(another.sectionIndex() == 30); REQUIRE(another.offset() == Approx(0.8)); } From f79a01250bdcbf869cd71877bc8ea88de9984759 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 10:09:17 +0200 Subject: [PATCH 26/44] format --- include/bbp/sonata/compartment_sets.h | 10 +++++----- python/bindings.cpp | 20 +++++++++----------- src/compartment_sets.cpp | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 4b45cc0..8979003 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -12,10 +12,10 @@ class CompartmentSets; } // namespace detail /** * CompartmentLocation public API. - * + * * This class uniquely identifies a compartment by a set of gid, section_index and offset: - * - * - gid: Global ID of the cell (Neuron) to which the compartment belongs. No + * + * - gid: Global ID of the cell (Neuron) to which the compartment belongs. No * overlaps among populations. * - section_index: Absolute section index. Progressive index that uniquely identifies the section. * There is a mapping between neuron section names (i.e. dend[10]) and this index. @@ -141,8 +141,8 @@ class SONATA_API CompartmentSets CompartmentSets(const CompartmentSets& other) = delete; CompartmentSets& operator=(CompartmentSets&&) noexcept; ~CompartmentSets(); - - /// Create new CompartmentSets from file. In this way we distinguish from + + /// Create new CompartmentSets from file. In this way we distinguish from /// the basic string constructor. static CompartmentSets fromFile(const std::string& path); diff --git a/python/bindings.cpp b/python/bindings.cpp index 59439d5..6a07e9c 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -547,11 +547,13 @@ PYBIND11_MODULE(_libsonata, m) { .def("materialize", &NodeSets::materialize, DOC_NODESETS(materialize)) .def("update", &NodeSets::update, "other"_a, DOC_NODESETS(update)) .def("toJSON", &NodeSets::toJSON, DOC_NODESETS(toJSON)); - + py::class_(m, "CompartmentLocation") .def(py::init()) .def(py::init(), - py::arg("gid"), py::arg("section_index"), py::arg("offset")) + py::arg("gid"), + py::arg("section_index"), + py::arg("offset")) .def_property_readonly("gid", &CompartmentLocation::gid, DOC_COMPARTMENTLOCATION(gid)) .def_property_readonly("section_index", &CompartmentLocation::sectionIndex, @@ -566,22 +568,18 @@ PYBIND11_MODULE(_libsonata, m) { }) .def("__eq__", &CompartmentLocation::operator==) .def("__ne__", &CompartmentLocation::operator!=) - .def("__copy__", [](const CompartmentLocation& self) { - return CompartmentLocation(self); - }) + .def("__copy__", [](const CompartmentLocation& self) { return CompartmentLocation(self); }) .def("__deepcopy__", - [](const CompartmentLocation& self, py::dict /* memo */) { - return CompartmentLocation(self); - }) + [](const CompartmentLocation& self, py::dict /* memo */) { + return CompartmentLocation(self); + }) .def("__repr__", [](const CompartmentLocation& self) { return py::str("CompartmentLocation({}, {}, {})") .format(self.gid(), self.sectionIndex(), self.offset()); }) .def("__str__", - [](const CompartmentLocation& self) { - return py::str(py::repr(py::cast(self))); - }); + [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); }); py::class_(m, "CompartmentSet") .def(py::init()) diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index da93108..f4d917b 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -434,7 +434,7 @@ CompartmentLocation::CompartmentLocation() = default; CompartmentLocation::CompartmentLocation(const int64_t gid, const int64_t section_index, const double offset) - : impl_(new detail::CompartmentLocation(gid, section_index, offset)) {} + : impl_(new detail::CompartmentLocation(gid, section_index, offset)) { } CompartmentLocation::CompartmentLocation(const std::string& content) : impl_(new detail::CompartmentLocation(content)) {} CompartmentLocation::CompartmentLocation(std::unique_ptr&& impl) From c56031a76db1a23a1c746ad34b9dbce40adbab4a Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 10:21:53 +0200 Subject: [PATCH 27/44] gid -> node_id --- include/bbp/sonata/compartment_sets.h | 14 ++++---- include/bbp/sonata/selection.h | 8 ++--- python/bindings.cpp | 14 ++++---- python/generated/docstrings.h | 4 +-- python/tests/test_compartment_sets.py | 26 +++++++-------- src/compartment_sets.cpp | 48 +++++++++++++-------------- src/selection.cpp | 8 ++--- tests/test_compartment_sets.cpp | 38 ++++++++++----------- 8 files changed, 80 insertions(+), 80 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 8979003..19f4512 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -13,9 +13,9 @@ class CompartmentSets; /** * CompartmentLocation public API. * - * This class uniquely identifies a compartment by a set of gid, section_index and offset: + * This class uniquely identifies a compartment by a set of node_id, section_index and offset: * - * - gid: Global ID of the cell (Neuron) to which the compartment belongs. No + * - node_id: Global ID of the cell (Neuron) to which the compartment belongs. No * overlaps among populations. * - section_index: Absolute section index. Progressive index that uniquely identifies the section. * There is a mapping between neuron section names (i.e. dend[10]) and this index. @@ -25,7 +25,7 @@ class SONATA_API CompartmentLocation { public: CompartmentLocation(); - CompartmentLocation(const int64_t gid, const int64_t section_index, const double offset); + CompartmentLocation(const int64_t node_id, const int64_t section_index, const double offset); explicit CompartmentLocation(const std::string& content); explicit CompartmentLocation(std::unique_ptr&& impl); CompartmentLocation(const CompartmentLocation& other); @@ -37,7 +37,7 @@ class SONATA_API CompartmentLocation bool operator==(const CompartmentLocation& other) const noexcept; bool operator!=(const CompartmentLocation& other) const noexcept; - uint64_t gid() const; + uint64_t nodeId() const; uint64_t sectionIndex() const; double offset() const; @@ -79,8 +79,8 @@ class SONATA_API CompartmentSetFilteredIterator { * CompartmentSet public API. * * This class represents a set of compartment locations associated with a neuron population. - * Each compartment is uniquely defined by a (gid, section_index, offset) triplet. - * This API supports filtering based on a gid selection. + * Each compartment is uniquely defined by a (node_id, section_index, offset) triplet. + * This API supports filtering based on a node_id selection. */ class SONATA_API CompartmentSet { @@ -106,7 +106,7 @@ class SONATA_API CompartmentSet /// Access element by index. It returns a copy! CompartmentLocation operator[](std::size_t index) const; - bbp::sonata::Selection gids() const; + bbp::sonata::Selection nodeIds() const; CompartmentSet filter(const bbp::sonata::Selection& selection = bbp::sonata::Selection({})) const; diff --git a/include/bbp/sonata/selection.h b/include/bbp/sonata/selection.h index 7deb156..f0432db 100644 --- a/include/bbp/sonata/selection.h +++ b/include/bbp/sonata/selection.h @@ -45,11 +45,11 @@ class SONATA_API Selection bool empty() const; /** - * Check if Selection contains a given GID - * @param gid is the GID to check - * @return true if Selection contains gid, false otherwise + * Check if Selection contains a given node id + * @param node id to check + * @return true if Selection contains the node id, false otherwise */ - bool contains(Value gid) const; + bool contains(Value node_id) const; private: Ranges ranges_; diff --git a/python/bindings.cpp b/python/bindings.cpp index 6a07e9c..e9a8860 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -477,8 +477,8 @@ PYBIND11_MODULE(_libsonata, m) { .def_property_readonly("flat_size", &Selection::flatSize, DOC_SEL(flatSize)) .def( "__contains__", - [](const Selection& sel, uint64_t gid) { return sel.contains(gid); }, - "Check if a GID is contained in the selection") + [](const Selection& sel, uint64_t node_id) { return sel.contains(node_id); }, + "Check if a node id is contained in the selection") .def( "__bool__", [](const Selection& obj) { return !obj.empty(); }, @@ -551,10 +551,10 @@ PYBIND11_MODULE(_libsonata, m) { py::class_(m, "CompartmentLocation") .def(py::init()) .def(py::init(), - py::arg("gid"), + py::arg("node_id"), py::arg("section_index"), py::arg("offset")) - .def_property_readonly("gid", &CompartmentLocation::gid, DOC_COMPARTMENTLOCATION(gid)) + .def_property_readonly("node_id", &CompartmentLocation::nodeId, DOC_COMPARTMENTLOCATION(nodeId)) .def_property_readonly("section_index", &CompartmentLocation::sectionIndex, DOC_COMPARTMENTLOCATION(sectionIndex)) @@ -564,7 +564,7 @@ PYBIND11_MODULE(_libsonata, m) { .def("toJSON", &CompartmentLocation::toJSON, DOC_COMPARTMENTLOCATION(toJSON)) .def("__iter__", [](const CompartmentLocation& self) { - return py::iter(py::make_tuple(self.gid(), self.sectionIndex(), self.offset())); + return py::iter(py::make_tuple(self.nodeId(), self.sectionIndex(), self.offset())); }) .def("__eq__", &CompartmentLocation::operator==) .def("__ne__", &CompartmentLocation::operator!=) @@ -576,7 +576,7 @@ PYBIND11_MODULE(_libsonata, m) { .def("__repr__", [](const CompartmentLocation& self) { return py::str("CompartmentLocation({}, {}, {})") - .format(self.gid(), self.sectionIndex(), self.offset()); + .format(self.nodeId(), self.sectionIndex(), self.offset()); }) .def("__str__", [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); }); @@ -590,7 +590,7 @@ PYBIND11_MODULE(_libsonata, m) { py::overload_cast(&CompartmentSet::size, py::const_), py::arg("selection") = bbp::sonata::Selection({}), DOC_COMPARTMENTSET(size)) - .def("gids", &CompartmentSet::gids, DOC_COMPARTMENTSET(gids)) + .def("node_ids", &CompartmentSet::nodeIds, DOC_COMPARTMENTSET(nodeIds)) .def("filter", &CompartmentSet::filter, py::arg("selection") = bbp::sonata::Selection({}), diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 18cdbed..9bcaa64 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -381,7 +381,7 @@ static const char *__doc_bbp_sonata_NodeSets_operator_assign = R"doc()doc"; static const char *__doc_bbp_sonata_NodeSets_toJSON = R"doc(Return the nodesets as a JSON string.)doc"; -static const char *__doc_bbp_sonata_CompartmentLocation_gid = R"doc(GID)doc"; +static const char *__doc_bbp_sonata_CompartmentLocation_nodeId = R"doc(Id of the node.)doc"; static const char *__doc_bbp_sonata_CompartmentLocation_sectionIndex = R"doc(Absolute section index. Progressive index that uniquely identifies the section. There is a mapping between neuron section names (i.e. dend[10]) and this index.)doc"; @@ -397,7 +397,7 @@ static const char *__doc_bbp_sonata_CompartmentSet_size = R"doc(Return the size static const char *__doc_bbp_sonata_CompartmentSet_getitem = R"doc("Get a CompartmentLocation by index. Creates a copy of the object.")doc"; -static const char *__doc_bbp_sonata_CompartmentSet_gids = R"doc(Gids in the list of CompartmentLocations.)doc"; +static const char *__doc_bbp_sonata_CompartmentSet_nodeIds = R"doc(Node ids in the list of CompartmentLocations.)doc"; static const char *__doc_bbp_sonata_CompartmentSet_filter = R"doc(Filter the compartment set based on a selection.)doc"; diff --git a/python/tests/test_compartment_sets.py b/python/tests/test_compartment_sets.py index 28b7c3a..6ba502f 100644 --- a/python/tests/test_compartment_sets.py +++ b/python/tests/test_compartment_sets.py @@ -15,13 +15,13 @@ class TestCompartmentLocation(unittest.TestCase): def test_constructor_from_values(self): loc = CompartmentLocation(4, 40, 0.9) - self.assertEqual(loc.gid, 4) + self.assertEqual(loc.node_id, 4) self.assertEqual(loc.section_index, 40) self.assertAlmostEqual(loc.offset, 0.9) def test_constructor_from_string(self): loc = CompartmentLocation("[4, 40, 0.9]") - self.assertEqual(loc.gid, 4) + self.assertEqual(loc.node_id, 4) self.assertEqual(loc.section_index, 40) self.assertAlmostEqual(loc.offset, 0.9) @@ -44,8 +44,8 @@ def test_repr_and_str(self): def test_iterable(self): loc = CompartmentLocation(4, 40, 0.9) - gid, section_index, offset = loc - self.assertEqual(gid, 4) + node_id, section_index, offset = loc + self.assertEqual(node_id, 4) self.assertEqual(section_index, 40) self.assertAlmostEqual(offset, 0.9) @@ -98,11 +98,11 @@ def test_len_dunder(self): def test_getitem(self): loc = self.cs[0] - self.assertEqual((loc.gid, loc.section_index, loc.offset), (1, 10, 0.5)) + self.assertEqual((loc.node_id, loc.section_index, loc.offset), (1, 10, 0.5)) def test_getitem_negative_index(self): loc = self.cs[-1] - self.assertEqual((loc.gid, loc.section_index, loc.offset), (2, 20, 0.25)) + self.assertEqual((loc.node_id, loc.section_index, loc.offset), (2, 20, 0.25)) def test_getitem_out_of_bounds_raises(self): with self.assertRaises(IndexError): @@ -111,16 +111,16 @@ def test_getitem_out_of_bounds_raises(self): _ = self.cs[-10] def test_iterators(self): - gids = [loc.gid for loc in self.cs] - self.assertEqual(gids, [1, 2, 3, 2]) - gids = [loc.gid for loc in self.cs.filtered_iter([2, 3])] - self.assertEqual(gids, [2, 3, 2]) + node_ids = [loc.node_id for loc in self.cs] + self.assertEqual(node_ids, [1, 2, 3, 2]) + node_ids = [loc.node_id for loc in self.cs.filtered_iter([2, 3])] + self.assertEqual(node_ids, [2, 3, 2]) conv_to_list = list(self.cs.filtered_iter([2, 3])) self.assertTrue(all(isinstance(i, CompartmentLocation) for i in conv_to_list)) - def test_gids(self): - gids = self.cs.gids() - self.assertEqual(gids, Selection([1, 2, 3])) + def test_node_ids(self): + node_ids = self.cs.node_ids() + self.assertEqual(node_ids, Selection([1, 2, 3])) def test_filter_identity(self): filtered = self.cs.filter() diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index f4d917b..04057e4 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -20,15 +20,15 @@ using json = nlohmann::json; class CompartmentLocation { private: - std::uint64_t gid_; + std::uint64_t node_id_; std::uint64_t section_index_; double offset_; - void setGid(int64_t gid) { - if (gid < 0) { - throw SonataError(fmt::format("GID must be non-negative, got {}", gid)); + void setNodeId(int64_t node_id) { + if (node_id < 0) { + throw SonataError(fmt::format("Node id must be non-negative, got {}", node_id)); } - gid_ = static_cast(gid); + node_id_ = static_cast(node_id); } void setSectionIndex(int64_t section_index) { if (section_index < 0) { @@ -52,8 +52,8 @@ class CompartmentLocation CompartmentLocation(const CompartmentLocation& other) = default; CompartmentLocation(CompartmentLocation&&) noexcept = default; CompartmentLocation& operator=(CompartmentLocation&&) noexcept = default; - CompartmentLocation(int64_t gid, int64_t section_index, double offset) { - setGid(gid); + CompartmentLocation(int64_t node_id, int64_t section_index, double offset) { + setNodeId(node_id); setSectionIndex(section_index); setOffset(offset); } @@ -64,11 +64,11 @@ class CompartmentLocation CompartmentLocation(const nlohmann::json& j) { if (!j.is_array() || j.size() != 3) { throw SonataError( - "CompartmentLocation must be an array of exactly 3 elements: [gid, section_index, " + "CompartmentLocation must be an array of exactly 3 elements: [node_id, section_index, " "offset]"); } - setGid(get_int64_or_throw(j[0])); + setNodeId(get_int64_or_throw(j[0])); setSectionIndex(get_int64_or_throw(j[1])); if (!j[2].is_number()) { @@ -77,8 +77,8 @@ class CompartmentLocation setOffset(j[2].get()); } - uint64_t gid() const { - return gid_; + uint64_t nodeId() const { + return node_id_; } uint64_t sectionIndex() const { @@ -90,11 +90,11 @@ class CompartmentLocation } nlohmann::json to_json() const { - return nlohmann::json::array({gid_, section_index_, offset_}); + return nlohmann::json::array({node_id_, section_index_, offset_}); } bool operator==(const CompartmentLocation& other) const { - return gid_ == other.gid_ && section_index_ == other.section_index_ && + return node_id_ == other.node_id_ && section_index_ == other.section_index_ && std::abs(offset_ - other.offset_) < offsetTolerance; } bool operator!=(const CompartmentLocation& other) const { @@ -121,7 +121,7 @@ class CompartmentSetFilteredIterator { void skip_to_valid() { while (current_ != end_) { - if (selection_.empty() || selection_.contains(current_->gid())) { + if (selection_.empty() || selection_.contains(current_->nodeId())) { break; } ++current_; @@ -241,7 +241,7 @@ class CompartmentSet { return static_cast(std::count_if(compartment_locations_.begin(), compartment_locations_.end(), [&](const CompartmentLocation& loc) { - return selection.contains(loc.gid()); + return selection.contains(loc.nodeId()); })); } @@ -253,13 +253,13 @@ class CompartmentSet { return compartment_locations_.at(index).clone(); } - Selection gids() const { + Selection nodeIds() const { std::vector result; std::unordered_set seen; result.reserve(compartment_locations_.size()); for (const auto& elem : compartment_locations_) { - uint64_t id = elem.gid(); + uint64_t id = elem.nodeId(); if (seen.insert(id).second) { // insert returns {iterator, bool} result.push_back(id); } @@ -295,7 +295,7 @@ class CompartmentSet { std::vector filtered; filtered.reserve(compartment_locations_.size()); for (const auto& el : compartment_locations_) { - if (selection.contains(el.gid())) { + if (selection.contains(el.nodeId())) { filtered.emplace_back(el); } } @@ -431,10 +431,10 @@ class CompartmentSets // CompartmentLocation public API CompartmentLocation::CompartmentLocation() = default; -CompartmentLocation::CompartmentLocation(const int64_t gid, +CompartmentLocation::CompartmentLocation(const int64_t node_id, const int64_t section_index, const double offset) - : impl_(new detail::CompartmentLocation(gid, section_index, offset)) { } + : impl_(new detail::CompartmentLocation(node_id, section_index, offset)) { } CompartmentLocation::CompartmentLocation(const std::string& content) : impl_(new detail::CompartmentLocation(content)) {} CompartmentLocation::CompartmentLocation(std::unique_ptr&& impl) @@ -460,8 +460,8 @@ bool CompartmentLocation::operator!=(const CompartmentLocation& other) const noe return *impl_ != *(other.impl_); } -uint64_t CompartmentLocation::gid() const { - return impl_->gid(); +uint64_t CompartmentLocation::nodeId() const { + return impl_->nodeId(); } uint64_t CompartmentLocation::sectionIndex() const { @@ -562,8 +562,8 @@ CompartmentLocation CompartmentSet::operator[](std::size_t index) const { return CompartmentLocation((*impl_)[index]); } -bbp::sonata::Selection CompartmentSet::gids() const { - return impl_->gids(); +bbp::sonata::Selection CompartmentSet::nodeIds() const { + return impl_->nodeIds(); } CompartmentSet CompartmentSet::filter(const bbp::sonata::Selection& selection) const { diff --git a/src/selection.cpp b/src/selection.cpp index ae671f3..8fb855d 100644 --- a/src/selection.cpp +++ b/src/selection.cpp @@ -118,13 +118,13 @@ Selection operator|(const Selection& lhs, const Selection& rhs) { return detail::union_(lhs.ranges(), rhs.ranges()); } -bool Selection::contains(Value gid) const { +bool Selection::contains(Value node_id) const { auto it = - std::lower_bound(ranges_.begin(), ranges_.end(), gid, [](const Range& range, Value v) { - return range[1] <= v; // Keep searching if gid >= end + std::lower_bound(ranges_.begin(), ranges_.end(), node_id, [](const Range& range, Value v) { + return range[1] <= v; // Keep searching if node_id >= end }); - return it != ranges_.end() && (*it)[0] <= gid && gid < (*it)[1]; + return it != ranges_.end() && (*it)[0] <= node_id && node_id < (*it)[1]; } } // namespace sonata diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index afc09cd..6eb9e76 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -9,9 +9,9 @@ using json = nlohmann::json; TEST_CASE("CompartmentLocation public API") { - SECTION("Construct from valid gid, section_idx, offset") { + SECTION("Construct from valid nodeId, section_idx, offset") { CompartmentLocation loc(1, 10, 0.5); - REQUIRE(loc.gid() == 1); + REQUIRE(loc.nodeId() == 1); REQUIRE(loc.sectionIndex() == 10); REQUIRE(loc.offset() == Approx(0.5)); } @@ -19,13 +19,13 @@ TEST_CASE("CompartmentLocation public API") { SECTION("Construct from valid JSON string") { std::string json_str = "[1, 10, 0.5]"; CompartmentLocation loc(json_str); - REQUIRE(loc.gid() == 1); + REQUIRE(loc.nodeId() == 1); REQUIRE(loc.sectionIndex() == 10); REQUIRE(loc.offset() == Approx(0.5)); } SECTION("Invalid JSON string throws") { - REQUIRE_THROWS_AS(CompartmentLocation("{\"gid\": 1}"), SonataError); + REQUIRE_THROWS_AS(CompartmentLocation("{\"nodeId\": 1}"), SonataError); REQUIRE_THROWS_AS(CompartmentLocation("[1, 2]"), SonataError); REQUIRE_THROWS_AS(CompartmentLocation("[1, 2, 0.1, 1]"), SonataError); REQUIRE_THROWS_AS(CompartmentLocation("[\"a\", 2, 0.5]"), SonataError); @@ -63,14 +63,14 @@ TEST_CASE("CompartmentLocation public API") { CompartmentLocation original(3, 30, 0.8); CompartmentLocation moved_constructed(std::move(original)); - REQUIRE(moved_constructed.gid() == 3); + REQUIRE(moved_constructed.nodeId() == 3); REQUIRE(moved_constructed.sectionIndex() == 30); REQUIRE(moved_constructed.offset() == Approx(0.8)); CompartmentLocation another(0, 0, 0); another = std::move(moved_constructed); - REQUIRE(another.gid() == 3); + REQUIRE(another.nodeId() == 3); REQUIRE(another.sectionIndex() == 30); REQUIRE(another.offset() == Approx(0.8)); } @@ -161,19 +161,19 @@ TEST_CASE("CompartmentSet public API") { auto pp = cs.filtered_crange(); - std::vector gids; + std::vector nodeIds; for (auto it = pp.first; it != pp.second; ++it) { - gids.push_back((*it).gid()); + nodeIds.push_back((*it).nodeId()); } - REQUIRE(gids.size() == 4); - REQUIRE((gids == std::vector{1, 2, 3, 2})); - gids.clear(); + REQUIRE(nodeIds.size() == 4); + REQUIRE((nodeIds == std::vector{1, 2, 3, 2})); + nodeIds.clear(); for (auto it = cs.filtered_crange(Selection::fromValues({2, 3})).first; it != pp.second; ++it) { - gids.push_back((*it).gid()); + nodeIds.push_back((*it).nodeId()); } - REQUIRE(gids.size() == 3); - REQUIRE((gids == std::vector{2, 3, 2})); + REQUIRE(nodeIds.size() == 3); + REQUIRE((nodeIds == std::vector{2, 3, 2})); } SECTION("Filter returns subset") { @@ -182,13 +182,13 @@ TEST_CASE("CompartmentSet public API") { REQUIRE(filtered.size() == 3); - // Check filtered compartments only contain gids 1 and 2 - auto gids = filtered.gids().flatten(); - REQUIRE(gids == std::vector({2, 3})); + // Check filtered compartments only contain nodeIds 1 and 2 + auto nodeIds = filtered.nodeIds().flatten(); + REQUIRE(nodeIds == std::vector({2, 3})); auto no_filtered = cs.filter(); REQUIRE(no_filtered.size() == 4); - auto no_filtered_gids = no_filtered.gids().flatten(); - REQUIRE(no_filtered_gids == std::vector({1, 2, 3})); + auto no_filtered_nodeIds = no_filtered.nodeIds().flatten(); + REQUIRE(no_filtered_nodeIds == std::vector({1, 2, 3})); } SECTION("Equality and inequality operators") { From bdf9596041d17e82ea01024af105478a470bb92f Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 10:22:11 +0200 Subject: [PATCH 28/44] format --- python/bindings.cpp | 4 +++- src/compartment_sets.cpp | 31 ++++++++++++++++--------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/python/bindings.cpp b/python/bindings.cpp index e9a8860..7132a5d 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -554,7 +554,9 @@ PYBIND11_MODULE(_libsonata, m) { py::arg("node_id"), py::arg("section_index"), py::arg("offset")) - .def_property_readonly("node_id", &CompartmentLocation::nodeId, DOC_COMPARTMENTLOCATION(nodeId)) + .def_property_readonly("node_id", + &CompartmentLocation::nodeId, + DOC_COMPARTMENTLOCATION(nodeId)) .def_property_readonly("section_index", &CompartmentLocation::sectionIndex, DOC_COMPARTMENTLOCATION(sectionIndex)) diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 04057e4..7b527ed 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -20,16 +20,16 @@ using json = nlohmann::json; class CompartmentLocation { private: - std::uint64_t node_id_; - std::uint64_t section_index_; - double offset_; - - void setNodeId(int64_t node_id) { - if (node_id < 0) { - throw SonataError(fmt::format("Node id must be non-negative, got {}", node_id)); - } - node_id_ = static_cast(node_id); - } + std::uint64_t node_id_; + std::uint64_t section_index_; + double offset_; + + void setNodeId(int64_t node_id) { + if (node_id < 0) { + throw SonataError(fmt::format("Node id must be non-negative, got {}", node_id)); + } + node_id_ = static_cast(node_id); + } void setSectionIndex(int64_t section_index) { if (section_index < 0) { throw SonataError( @@ -64,7 +64,8 @@ class CompartmentLocation CompartmentLocation(const nlohmann::json& j) { if (!j.is_array() || j.size() != 3) { throw SonataError( - "CompartmentLocation must be an array of exactly 3 elements: [node_id, section_index, " + "CompartmentLocation must be an array of exactly 3 elements: [node_id, " + "section_index, " "offset]"); } @@ -239,10 +240,10 @@ class CompartmentSet { } return static_cast(std::count_if(compartment_locations_.begin(), - compartment_locations_.end(), - [&](const CompartmentLocation& loc) { - return selection.contains(loc.nodeId()); - })); + compartment_locations_.end(), + [&](const CompartmentLocation& loc) { + return selection.contains(loc.nodeId()); + })); } std::size_t empty() const { From b84755e3cc72821b66e0dd0f2d8de5a1416c7515 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 10:59:00 +0200 Subject: [PATCH 29/44] more Mike's comments resolved --- include/bbp/sonata/compartment_sets.h | 8 ++++---- python/bindings.cpp | 2 +- python/generated/docstrings.h | 2 ++ src/utils.h | 17 +++++++++++++++-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 19f4512..c4b2cb5 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -92,10 +92,10 @@ class SONATA_API CompartmentSet explicit CompartmentSet(std::shared_ptr&& impl); std::pair - filtered_crange(bbp::sonata::Selection selection = bbp::sonata::Selection({})) const; + filtered_crange(Selection selection = Selection({})) const; /// Size of the set, optionally filtered by selection - std::size_t size(const bbp::sonata::Selection& selection = bbp::sonata::Selection({})) const; + std::size_t size(const Selection& selection = Selection({})) const; // Is empty? bool empty() const; @@ -106,9 +106,9 @@ class SONATA_API CompartmentSet /// Access element by index. It returns a copy! CompartmentLocation operator[](std::size_t index) const; - bbp::sonata::Selection nodeIds() const; + Selection nodeIds() const; - CompartmentSet filter(const bbp::sonata::Selection& selection = bbp::sonata::Selection({})) const; + CompartmentSet filter(const Selection& selection = Selection({})) const; /// Serialize to JSON string std::string toJSON() const; diff --git a/python/bindings.cpp b/python/bindings.cpp index 7132a5d..d826bfc 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -478,7 +478,7 @@ PYBIND11_MODULE(_libsonata, m) { .def( "__contains__", [](const Selection& sel, uint64_t node_id) { return sel.contains(node_id); }, - "Check if a node id is contained in the selection") + DOC_SEL(nodeId)) .def( "__bool__", [](const Selection& obj) { return !obj.empty(); }, diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 9bcaa64..014420f 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -728,6 +728,8 @@ static const char *__doc_bbp_sonata_Selection_ranges = R"doc(Get a list of range static const char *__doc_bbp_sonata_Selection_ranges_2 = R"doc()doc"; +static const char *__doc_bbp_sonata_Selection_nodeId = R"doc(Check if a node id is contained in the selection)doc"; + static const char *__doc_bbp_sonata_SimulationConfig = R"doc(Read access to a SONATA simulation config file.)doc"; static const char *__doc_bbp_sonata_SimulationConfig_Conditions = R"doc(Parameters defining global experimental conditions.)doc"; diff --git a/src/utils.h b/src/utils.h index b5feffe..d39aed7 100644 --- a/src/utils.h +++ b/src/utils.h @@ -53,10 +53,17 @@ inline int64_t get_int64_or_throw(const json& el) { if (!el.is_number()) { throw SonataError(fmt::format("expected integer, got {}", el.dump())); } + auto v = el.get(); if (std::floor(v) != v) { throw SonataError(fmt::format("expected integer, got float {}", v)); } + + if (v < static_cast(std::numeric_limits::min()) || + v > static_cast(std::numeric_limits::max())) { + throw SonataError(fmt::format("value {} out of int64_t bounds", v)); + } + return static_cast(v); } @@ -64,14 +71,20 @@ inline uint64_t get_uint64_or_throw(const json& el) { if (!el.is_number()) { throw SonataError(fmt::format("expected unsigned integer, got {}", el.dump())); } + auto v = el.get(); if (v < 0) { - throw SonataError(fmt::format("expected unsigned integer, got {}", v)); + throw SonataError(fmt::format("expected unsigned integer, got negative value {}", v)); } if (std::floor(v) != v) { - throw SonataError(fmt::format("expected integer, got float {}", v)); + throw SonataError(fmt::format("expected unsigned integer, got float {}", v)); + } + + if (v > static_cast(std::numeric_limits::max())) { + throw SonataError(fmt::format("value {} out of uint64_t bounds", v)); } + return static_cast(v); } From d0ac13ac3c43e8fb06050463b0893b4c0b2884ba Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 10:59:19 +0200 Subject: [PATCH 30/44] format --- include/bbp/sonata/compartment_sets.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index c4b2cb5..eaaa575 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -91,8 +91,8 @@ class SONATA_API CompartmentSet explicit CompartmentSet(const std::string& json_content); explicit CompartmentSet(std::shared_ptr&& impl); - std::pair - filtered_crange(Selection selection = Selection({})) const; + std::pair filtered_crange( + Selection selection = Selection({})) const; /// Size of the set, optionally filtered by selection std::size_t size(const Selection& selection = Selection({})) const; From 4fb9a1bfc7273272df95ff7c5434e49753d17031 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 11:26:29 +0200 Subject: [PATCH 31/44] remove glob --- CMakeLists.txt | 18 ++++++++++++++++-- tests/CMakeLists.txt | 11 ++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6a8a0c..aa037b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,8 +89,22 @@ endif() # Targets # ============================================================================= -file(GLOB SONATA_SRC "src/*.cpp") -list(APPEND SONATA_SRC "${CMAKE_CURRENT_BINARY_DIR}/src/version.cpp") +set(SONATA_SRC + src/common.cpp + src/compartment_sets.cpp + src/config.cpp + src/edge_index.cpp + src/edges.cpp + src/hdf5_mutex.cpp + src/hdf5_reader.cpp + src/node_sets.cpp + src/nodes.cpp + src/population.cpp + src/report_reader.cpp + src/selection.cpp + src/utils.cpp + ${CMAKE_CURRENT_BINARY_DIR}/src/version.cpp + ) configure_file ( ${CMAKE_CURRENT_SOURCE_DIR}/src/version.cpp.in diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e073549..667f28b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,4 +1,13 @@ -file(GLOB TESTS_SRC "*.cpp") +set(TESTS_SRC + main.cpp + test_compartment_sets.cpp + test_config.cpp + test_edges.cpp + test_node_sets.cpp + test_nodes.cpp + test_report_reader.cpp + test_selection.cpp +) if(NOT EXTLIB_FROM_SUBMODULES) # When using submodules `include(.../Catch)` is performed From 2bce919cdfb58975d08a43c05cfebf43726e107b Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 11:49:31 +0200 Subject: [PATCH 32/44] fixup! selection.contains: ranges are not sorted for sure --- src/selection.cpp | 7 ++++-- tests/test_selection.cpp | 52 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/selection.cpp b/src/selection.cpp index 8fb855d..77b3123 100644 --- a/src/selection.cpp +++ b/src/selection.cpp @@ -119,12 +119,15 @@ Selection operator|(const Selection& lhs, const Selection& rhs) { } bool Selection::contains(Value node_id) const { + Ranges ret; + std::copy(ranges_.begin(), ranges_.end(), std::back_inserter(ret)); + ret = detail::_sortAndMerge(ret); auto it = - std::lower_bound(ranges_.begin(), ranges_.end(), node_id, [](const Range& range, Value v) { + std::lower_bound(ret.begin(), ret.end(), node_id, [](const Range& range, Value v) { return range[1] <= v; // Keep searching if node_id >= end }); - return it != ranges_.end() && (*it)[0] <= node_id && node_id < (*it)[1]; + return it != ret.end() && (*it)[0] <= node_id && node_id < (*it)[1]; } } // namespace sonata diff --git a/tests/test_selection.cpp b/tests/test_selection.cpp index 118e657..5504e16 100644 --- a/tests/test_selection.cpp +++ b/tests/test_selection.cpp @@ -102,6 +102,58 @@ TEST_CASE("Selection", "[base]") { CHECK(Selection({{0, 10}}) == (even | odd)); } + SECTION("union") { + const auto empty = Selection({}); + CHECK(empty == (empty | empty)); + + // clang-format off + // 1 2 + // 01234567890123456789012345 + // a = xx xxxxx xxxxxxxxxx x + // b = xxxxx xxxxx xxxxxxxx x + // xxxxxxxxxxxxxxxxxxxxxxx x + // clang-format on + const auto a = Selection({{24, 25}, {13, 23}, {5, 10}, {0, 2}}); + const auto b = Selection({{1, 6}, {8, 13}, {15, 23}, {24, 25}}); + CHECK(b == (b | empty)); // need to use b since it's sorted + CHECK(b == (empty | b)); + + const auto expected = Selection({{0, 23}, {24, 25}}); + CHECK(expected == (a | b)); + CHECK(expected == (b | a)); + + const auto odd = Selection::fromValues({1, 3, 5, 7, 9}); + const auto even = Selection::fromValues({0, 2, 4, 6, 8}); + CHECK(Selection({{0, 10}}) == (odd | even)); + CHECK(Selection({{0, 10}}) == (even | odd)); + } + + SECTION("contains") { + const auto sel = Selection({{2, 5}, {20, 21}, {10, 15}}); // unsorted ranges + + // Inside ranges + CHECK(sel.contains(2)); + CHECK(sel.contains(3)); + CHECK(sel.contains(4)); + CHECK(sel.contains(10)); + CHECK(sel.contains(14)); + CHECK(sel.contains(20)); + + // Outside ranges + CHECK_FALSE(sel.contains(1)); + CHECK_FALSE(sel.contains(5)); // upper bound is exclusive + CHECK_FALSE(sel.contains(6)); + CHECK_FALSE(sel.contains(9)); + CHECK_FALSE(sel.contains(15)); // upper bound is exclusive + CHECK_FALSE(sel.contains(19)); + CHECK_FALSE(sel.contains(21)); // upper bound is exclusive + + // Edge case: empty selection + const auto empty = Selection({}); + CHECK_FALSE(empty.contains(0)); + CHECK_FALSE(empty.contains(100)); + } + /* need a way to test un-exported stuff SECTION("_sortAndMerge") { const auto empty = Selection::Ranges({}); From c9099443986cfdfef30b19aef92fe3be8f65762c Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 11:51:44 +0200 Subject: [PATCH 33/44] format --- src/selection.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/selection.cpp b/src/selection.cpp index 77b3123..a527652 100644 --- a/src/selection.cpp +++ b/src/selection.cpp @@ -122,10 +122,9 @@ bool Selection::contains(Value node_id) const { Ranges ret; std::copy(ranges_.begin(), ranges_.end(), std::back_inserter(ret)); ret = detail::_sortAndMerge(ret); - auto it = - std::lower_bound(ret.begin(), ret.end(), node_id, [](const Range& range, Value v) { - return range[1] <= v; // Keep searching if node_id >= end - }); + auto it = std::lower_bound(ret.begin(), ret.end(), node_id, [](const Range& range, Value v) { + return range[1] <= v; // Keep searching if node_id >= end + }); return it != ret.end() && (*it)[0] <= node_id && node_id < (*it)[1]; } From 9d76a4cf4156185293f5b3d8de6b13e08865eaf3 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 12:31:09 +0200 Subject: [PATCH 34/44] keys -> names --- include/bbp/sonata/compartment_sets.h | 4 ++-- python/bindings.cpp | 2 +- python/tests/test_compartment_sets.py | 8 ++++---- src/compartment_sets.cpp | 6 +++--- tests/test_compartment_sets.cpp | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index eaaa575..e354b2e 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -158,8 +158,8 @@ class SONATA_API CompartmentSets /// Check if key exists bool contains(const std::string& key) const; - /// Get keys as a vector (use vector here) - std::vector keys() const; + /// Get names of CompartmentSet(s) as a vector + std::vector names() const; /// Get all compartment sets as vector std::vector values() const; diff --git a/python/bindings.cpp b/python/bindings.cpp index d826bfc..e73d35a 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -655,7 +655,7 @@ PYBIND11_MODULE(_libsonata, m) { &CompartmentSets::contains, py::arg("key"), DOC_COMPARTMENTSETS(contains)) - .def("keys", &CompartmentSets::keys) + .def("names", &CompartmentSets::names) .def("values", &CompartmentSets::values) .def("items", &CompartmentSets::items) .def("toJSON", &CompartmentSets::toJSON, DOC_COMPARTMENTSETS(toJSON)) diff --git a/python/tests/test_compartment_sets.py b/python/tests/test_compartment_sets.py index 6ba502f..f7b3a2c 100644 --- a/python/tests/test_compartment_sets.py +++ b/python/tests/test_compartment_sets.py @@ -174,20 +174,20 @@ def test_init_from_string(self): def test_contains(self): - keys = self.cs.keys() + keys = self.cs.names() for key in keys: self.assertIn(key, self.cs) self.assertNotIn('non_existing_key', self.cs) def test_getitem(self): - keys = self.cs.keys() + keys = self.cs.names() if keys: key = keys[0] val = self.cs[key] self.assertIsInstance(val, CompartmentSet) def test_keys_values_items(self): - keys = self.cs.keys() + keys = self.cs.names() values = self.cs.values() items = self.cs.items() self.assertEqual(len(keys), len(values)) @@ -228,5 +228,5 @@ def test_repr_and_str(self): self.assertTrue(r.startswith("CompartmentSets({")) self.assertEqual(s, r) # str delegates to repr # repr should contain keys from the dict - for key in self.cs.keys(): + for key in self.cs.names(): self.assertIn(str(key), r) \ No newline at end of file diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 7b527ed..8ad569d 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -363,7 +363,7 @@ class CompartmentSets return data_.empty(); } - std::vector keys() const { + std::vector names() const { std::vector result; result.reserve(data_.size()); std::transform(data_.begin(), data_.end(), std::back_inserter(result), @@ -622,8 +622,8 @@ bool CompartmentSets::contains(const std::string& key) const { } // Get keys as set or vector (use vector here) -std::vector CompartmentSets::keys() const { - return impl_->keys(); +std::vector CompartmentSets::names() const { + return impl_->names(); } // Get all compartment sets as vector diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index 6eb9e76..3c38618 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -288,8 +288,8 @@ TEST_CASE("CompartmentSets public API") { CHECK(sets.size() == 2); CHECK_FALSE(sets.empty()); - auto keys = sets.keys(); - REQUIRE(sets.keys() == std::vector{"cs0", "cs1"}); + auto keys = sets.names(); + REQUIRE(sets.names() == std::vector{"cs0", "cs1"}); CHECK(sets.contains("cs0")); CHECK(sets.contains("cs1")); @@ -370,7 +370,7 @@ TEST_CASE("CompartmentSets public API") { SECTION("Keys returns correct vector") { CompartmentSets sets(json); - auto keys = sets.keys(); + auto keys = sets.names(); CHECK(keys == std::vector{"cs0", "cs1"}); } From b91ed52199cd0e767a87b0ab0bc541f480016d2a Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 13:58:49 +0200 Subject: [PATCH 35/44] py::str -> fmt::format --- python/bindings.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/bindings.cpp b/python/bindings.cpp index e73d35a..1f2a736 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -576,10 +576,10 @@ PYBIND11_MODULE(_libsonata, m) { return CompartmentLocation(self); }) .def("__repr__", - [](const CompartmentLocation& self) { - return py::str("CompartmentLocation({}, {}, {})") - .format(self.nodeId(), self.sectionIndex(), self.offset()); - }) + [](const CompartmentLocation& self) { + return fmt::format("CompartmentLocation({}, {}, {})", + self.nodeId(), self.sectionIndex(), self.offset()); + }) .def("__str__", [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); }); From ed789f67b69373afa680ad8c8a9d51559ba4e519 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 13:59:47 +0200 Subject: [PATCH 36/44] format --- python/bindings.cpp | 10 ++++++---- src/compartment_sets.cpp | 23 ++++++++++------------- tests/test_compartment_sets.cpp | 2 -- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/python/bindings.cpp b/python/bindings.cpp index 1f2a736..43921b8 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -576,10 +576,12 @@ PYBIND11_MODULE(_libsonata, m) { return CompartmentLocation(self); }) .def("__repr__", - [](const CompartmentLocation& self) { - return fmt::format("CompartmentLocation({}, {}, {})", - self.nodeId(), self.sectionIndex(), self.offset()); - }) + [](const CompartmentLocation& self) { + return fmt::format("CompartmentLocation({}, {}, {})", + self.nodeId(), + self.sectionIndex(), + self.offset()); + }) .def("__str__", [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); }); diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 8ad569d..3d341c7 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -45,18 +45,15 @@ class CompartmentLocation offset_ = offset; } - public: - static constexpr double offsetTolerance = 1e-4; - // static constexpr double offsetToleranceInv = 1.0 / offsetTolerance; - - CompartmentLocation(const CompartmentLocation& other) = default; - CompartmentLocation(CompartmentLocation&&) noexcept = default; - CompartmentLocation& operator=(CompartmentLocation&&) noexcept = default; - CompartmentLocation(int64_t node_id, int64_t section_index, double offset) { - setNodeId(node_id); - setSectionIndex(section_index); - setOffset(offset); - } + public: + CompartmentLocation(const CompartmentLocation& other) = default; + CompartmentLocation(CompartmentLocation&&) noexcept = default; + CompartmentLocation& operator=(CompartmentLocation&&) noexcept = default; + CompartmentLocation(int64_t node_id, int64_t section_index, double offset) { + setNodeId(node_id); + setSectionIndex(section_index); + setOffset(offset); + } CompartmentLocation(const std::string& content) : CompartmentLocation(json::parse(content)) {} @@ -96,7 +93,7 @@ class CompartmentLocation bool operator==(const CompartmentLocation& other) const { return node_id_ == other.node_id_ && section_index_ == other.section_index_ && - std::abs(offset_ - other.offset_) < offsetTolerance; + offset_ == other.offset_; } bool operator!=(const CompartmentLocation& other) const { return !(*this == other); diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index 3c38618..2dc1172 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -41,12 +41,10 @@ TEST_CASE("CompartmentLocation public API") { CompartmentLocation loc1(1, 10, 0.5); CompartmentLocation loc2(1, 10, 0.5); CompartmentLocation loc3(1, 10, 0.6); - CompartmentLocation loc4(1, 10, 0.5000001); REQUIRE(loc1 == loc2); REQUIRE_FALSE(loc1 != loc2); REQUIRE(loc1 != loc3); - REQUIRE(loc1 == loc4); } SECTION("Copy constructor and assignment") { From d4b729a371d11358452acf59d50872bf2c1dde03 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 14:57:23 +0200 Subject: [PATCH 37/44] remove iter copy and deepcopy --- python/bindings.cpp | 9 --------- python/tests/test_compartment_sets.py | 25 +------------------------ 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/python/bindings.cpp b/python/bindings.cpp index 43921b8..1979688 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -564,17 +564,8 @@ PYBIND11_MODULE(_libsonata, m) { &CompartmentLocation::offset, DOC_COMPARTMENTLOCATION(offset)) .def("toJSON", &CompartmentLocation::toJSON, DOC_COMPARTMENTLOCATION(toJSON)) - .def("__iter__", - [](const CompartmentLocation& self) { - return py::iter(py::make_tuple(self.nodeId(), self.sectionIndex(), self.offset())); - }) .def("__eq__", &CompartmentLocation::operator==) .def("__ne__", &CompartmentLocation::operator!=) - .def("__copy__", [](const CompartmentLocation& self) { return CompartmentLocation(self); }) - .def("__deepcopy__", - [](const CompartmentLocation& self, py::dict /* memo */) { - return CompartmentLocation(self); - }) .def("__repr__", [](const CompartmentLocation& self) { return fmt::format("CompartmentLocation({}, {}, {})", diff --git a/python/tests/test_compartment_sets.py b/python/tests/test_compartment_sets.py index f7b3a2c..8194a07 100644 --- a/python/tests/test_compartment_sets.py +++ b/python/tests/test_compartment_sets.py @@ -42,13 +42,6 @@ def test_repr_and_str(self): self.assertEqual(repr(loc), expected) self.assertEqual(str(loc), repr(loc)) # str should delegate to repr - def test_iterable(self): - loc = CompartmentLocation(4, 40, 0.9) - node_id, section_index, offset = loc - self.assertEqual(node_id, 4) - self.assertEqual(section_index, 40) - self.assertAlmostEqual(offset, 0.9) - def test_assignment_creates_copy(self): loc1 = CompartmentLocation(1, 2, 0.3) loc2 = loc1 # This is a reference assignment in Python @@ -57,20 +50,6 @@ def test_assignment_creates_copy(self): # Now mutate loc1 and check if loc2 is affected — which it will be, unless toJSON etc. are implemented with deep semantics self.assertIs(loc1, loc2) # They reference the same object - def test_explicit_copy(self): - import copy - loc1 = CompartmentLocation(1, 2, 0.3) - loc2 = copy.copy(loc1) - self.assertEqual(loc1, loc2) - self.assertIsNot(loc1, loc2) # Ensure they’re distinct objects - - def test_deepcopy(self): - import copy - loc1 = CompartmentLocation(1, 2, 0.3) - loc2 = copy.deepcopy(loc1) - self.assertEqual(loc1, loc2) - self.assertIsNot(loc1, loc2) - class TestCompartmentSet(unittest.TestCase): def setUp(self): self.json = '''{ @@ -131,9 +110,7 @@ def test_filter_identity(self): def test_toJSON_roundtrip(self): json_out = self.cs.toJSON() cs2 = CompartmentSet(json_out) - self.assertEqual(len(cs2), 4) - self.assertEqual(cs2.population, self.cs.population) - self.assertEqual([tuple(loc) for loc in cs2], [tuple(loc) for loc in self.cs]) + self.assertEqual(cs2, self.cs) def test_equality(self): cs1 = CompartmentSet(self.json) From 6ada577f77ef25c3a25b602ca6bb0b750a3e2962 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 16:31:37 +0200 Subject: [PATCH 38/44] CompartmentLocation unification impl and public api --- include/bbp/sonata/compartment_sets.h | 76 +++++++---- python/CMakeLists.txt | 1 + src/compartment_sets.cpp | 179 +++++++------------------- 3 files changed, 93 insertions(+), 163 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index e354b2e..898ee24 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -2,10 +2,11 @@ #include +#include + namespace bbp { namespace sonata { namespace detail { -class CompartmentLocation; class CompartmentSetFilteredIterator; class CompartmentSet; class CompartmentSets; @@ -23,37 +24,57 @@ class CompartmentSets; */ class SONATA_API CompartmentLocation { - public: - CompartmentLocation(); - CompartmentLocation(const int64_t node_id, const int64_t section_index, const double offset); - explicit CompartmentLocation(const std::string& content); - explicit CompartmentLocation(std::unique_ptr&& impl); - CompartmentLocation(const CompartmentLocation& other); - CompartmentLocation& operator=(const CompartmentLocation& other); - CompartmentLocation(CompartmentLocation&&) noexcept; - CompartmentLocation& operator=(CompartmentLocation&&) noexcept; - ~CompartmentLocation(); - - bool operator==(const CompartmentLocation& other) const noexcept; - bool operator!=(const CompartmentLocation& other) const noexcept; - - uint64_t nodeId() const; - uint64_t sectionIndex() const; - double offset() const; +private: + uint64_t node_id_ = 0; + uint64_t section_index_ = 0; + double offset_ = 0.0; - std::string toJSON() const; + void setNodeId(int64_t node_id); + void setSectionIndex(int64_t section_index); + void setOffset(double offset); - private: - std::unique_ptr impl_; +public: + explicit CompartmentLocation(int64_t node_id, int64_t section_index, double offset) { + setNodeId(node_id); + setSectionIndex(section_index); + setOffset(offset); + } + explicit CompartmentLocation(const nlohmann::json& j); + explicit CompartmentLocation(const std::string& content) : CompartmentLocation(nlohmann::json::parse(content)) {} + explicit CompartmentLocation(const char* content) : CompartmentLocation(std::string(content)) {} + + bool operator==(const CompartmentLocation& other) const noexcept { + return node_id_ == other.node_id_ && + section_index_ == other.section_index_ && + offset_ == other.offset_; + } + + bool operator!=(const CompartmentLocation& other) const noexcept { + return !(*this == other); + } + + uint64_t nodeId() const { return node_id_; } + uint64_t sectionIndex() const { return section_index_; } + double offset() const { return offset_; } + + /// Convenience function to transform the class in a json object + nlohmann::json to_json() const { + return nlohmann::json::array({node_id_, section_index_, offset_}); + } + + /// Convenience function to transform the class in a json-formatted string + std::string toJSON() const { + return to_json().dump(); + } }; class SONATA_API CompartmentSetFilteredIterator { public: - using iterator_category = std::input_iterator_tag; + using iterator_category = std::forward_iterator_tag; using value_type = CompartmentLocation; using difference_type = std::ptrdiff_t; - using pointer = void; // dereference returns by value - using reference = CompartmentLocation; // dereference returns by value + using pointer = const CompartmentLocation*; + using reference = const CompartmentLocation&; explicit CompartmentSetFilteredIterator(std::unique_ptr impl); CompartmentSetFilteredIterator(const CompartmentSetFilteredIterator& other); @@ -62,10 +83,9 @@ class SONATA_API CompartmentSetFilteredIterator { CompartmentSetFilteredIterator& operator=(CompartmentSetFilteredIterator&&) noexcept; ~CompartmentSetFilteredIterator(); - /// Dereference operator. It makes a copy! - CompartmentLocation operator*() const; - /// Arrow operator is voluntarely disabled because we can only return copies of CompartmentLocation. - /// In any way we need to find a location to store a temp CompartmentLocation and memory leaks become possible. + const CompartmentLocation& operator*() const; + const CompartmentLocation* operator->() const; + CompartmentSetFilteredIterator& operator++(); // prefix ++ CompartmentSetFilteredIterator operator++(int); // postfix ++ bool operator==(const CompartmentSetFilteredIterator& other) const; diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 70bde60..578c788 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -17,4 +17,5 @@ target_link_libraries(sonata_python PRIVATE HighFive PRIVATE fmt::fmt-header-only PRIVATE pybind11::module + PRIVATE nlohmann_json::nlohmann_json ) diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 3d341c7..3ad18a8 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -16,102 +16,14 @@ namespace detail { using json = nlohmann::json; - -class CompartmentLocation -{ - private: - std::uint64_t node_id_; - std::uint64_t section_index_; - double offset_; - - void setNodeId(int64_t node_id) { - if (node_id < 0) { - throw SonataError(fmt::format("Node id must be non-negative, got {}", node_id)); - } - node_id_ = static_cast(node_id); - } - void setSectionIndex(int64_t section_index) { - if (section_index < 0) { - throw SonataError( - fmt::format("Section index must be non-negative, got {}", section_index)); - } - section_index_ = static_cast(section_index); - } - void setOffset(double offset) { - if (offset < 0.0 || offset > 1.0) { - throw SonataError( - fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); - } - offset_ = offset; - } - - public: - CompartmentLocation(const CompartmentLocation& other) = default; - CompartmentLocation(CompartmentLocation&&) noexcept = default; - CompartmentLocation& operator=(CompartmentLocation&&) noexcept = default; - CompartmentLocation(int64_t node_id, int64_t section_index, double offset) { - setNodeId(node_id); - setSectionIndex(section_index); - setOffset(offset); - } - - CompartmentLocation(const std::string& content) - : CompartmentLocation(json::parse(content)) {} - - CompartmentLocation(const nlohmann::json& j) { - if (!j.is_array() || j.size() != 3) { - throw SonataError( - "CompartmentLocation must be an array of exactly 3 elements: [node_id, " - "section_index, " - "offset]"); - } - - setNodeId(get_int64_or_throw(j[0])); - setSectionIndex(get_int64_or_throw(j[1])); - - if (!j[2].is_number()) { - throw SonataError("Fourth element (offset) must be a number"); - } - setOffset(j[2].get()); - } - - uint64_t nodeId() const { - return node_id_; - } - - uint64_t sectionIndex() const { - return section_index_; - } - - double offset() const { - return offset_; - } - - nlohmann::json to_json() const { - return nlohmann::json::array({node_id_, section_index_, offset_}); - } - - bool operator==(const CompartmentLocation& other) const { - return node_id_ == other.node_id_ && section_index_ == other.section_index_ && - offset_ == other.offset_; - } - bool operator!=(const CompartmentLocation& other) const { - return !(*this == other); - } - - std::unique_ptr clone() const { - return std::unique_ptr(new CompartmentLocation(*this)); - } -}; - class CompartmentSetFilteredIterator { public: using base_iterator = std::vector::const_iterator; - using value_type = detail::CompartmentLocation; + using value_type = CompartmentLocation; using reference = const value_type&; using pointer = const value_type*; using difference_type = std::ptrdiff_t; - using iterator_category = std::input_iterator_tag; + using iterator_category = std::forward_iterator_tag; private: base_iterator current_; base_iterator end_; @@ -134,6 +46,8 @@ class CompartmentSetFilteredIterator { : current_(current), end_(end), selection_(std::move(selection)) { skip_to_valid(); } + CompartmentSetFilteredIterator(base_iterator end) + : current_(end), end_(end), selection_({}) /* empty selection */ {} reference operator*() const { return *current_; @@ -224,9 +138,7 @@ class CompartmentSet { CompartmentSetFilteredIterator begin_it(compartment_locations_.cbegin(), compartment_locations_.cend(), selection); - CompartmentSetFilteredIterator end_it(compartment_locations_.cend(), - compartment_locations_.cend(), - std::move(selection)); + CompartmentSetFilteredIterator end_it(compartment_locations_.cend()); return {begin_it, end_it}; } @@ -247,8 +159,8 @@ class CompartmentSet { return compartment_locations_.empty(); } - std::unique_ptr operator[](std::size_t index) const { - return compartment_locations_.at(index).clone(); + const CompartmentLocation& operator[](std::size_t index) const { + return compartment_locations_.at(index); } Selection nodeIds() const { @@ -428,50 +340,43 @@ class CompartmentSets } // namespace detail // CompartmentLocation public API -CompartmentLocation::CompartmentLocation() = default; -CompartmentLocation::CompartmentLocation(const int64_t node_id, - const int64_t section_index, - const double offset) - : impl_(new detail::CompartmentLocation(node_id, section_index, offset)) { } -CompartmentLocation::CompartmentLocation(const std::string& content) - : impl_(new detail::CompartmentLocation(content)) {} -CompartmentLocation::CompartmentLocation(std::unique_ptr&& impl) - : impl_(std::move(impl)) {} - -CompartmentLocation::CompartmentLocation(const CompartmentLocation& other) : impl_(other.impl_->clone()){} -CompartmentLocation& CompartmentLocation::operator=(const CompartmentLocation& other) { - if (this != &other) { - auto tmp = other.impl_->clone(); // create copy first, if it throws impl is not assigned - impl_ = std::move(tmp); // then assign - } - return *this; -} -CompartmentLocation::CompartmentLocation(CompartmentLocation&&) noexcept = default; -CompartmentLocation& CompartmentLocation::operator=(CompartmentLocation&&) noexcept = default; -CompartmentLocation::~CompartmentLocation() = default; -bool CompartmentLocation::operator==(const CompartmentLocation& other) const noexcept { - return *impl_ == *(other.impl_); -} +CompartmentLocation::CompartmentLocation(const nlohmann::json& j) { + if (!j.is_array() || j.size() != 3) { + throw SonataError( + "CompartmentLocation must be an array of exactly 3 elements: [node_id, " + "section_index, " + "offset]"); + } -bool CompartmentLocation::operator!=(const CompartmentLocation& other) const noexcept { - return *impl_ != *(other.impl_); -} + setNodeId(get_int64_or_throw(j[0])); + setSectionIndex(get_int64_or_throw(j[1])); -uint64_t CompartmentLocation::nodeId() const { - return impl_->nodeId(); -} + if (!j[2].is_number()) { + throw SonataError("Offset (third element) must be a number"); + } + setOffset(j[2].get()); + } -uint64_t CompartmentLocation::sectionIndex() const { - return impl_->sectionIndex(); +void CompartmentLocation::setNodeId(int64_t node_id) { + if (node_id < 0) { + throw SonataError(fmt::format("Node id must be non-negative, got {}", node_id)); + } + node_id_ = static_cast(node_id); } - -double CompartmentLocation::offset() const { - return impl_->offset(); +void CompartmentLocation::setSectionIndex(int64_t section_index) { + if (section_index < 0) { + throw SonataError( + fmt::format("Section index must be non-negative, got {}", section_index)); + } + section_index_ = static_cast(section_index); } - -std::string CompartmentLocation::toJSON() const { - return impl_->to_json().dump(); +void CompartmentLocation::setOffset(double offset) { + if (offset < 0.0 || offset > 1.0) { + throw SonataError( + fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); + } + offset_ = offset; } // CompartmentSetFilteredIterator public API @@ -501,8 +406,12 @@ CompartmentSetFilteredIterator& CompartmentSetFilteredIterator::operator=(Compar CompartmentSetFilteredIterator::~CompartmentSetFilteredIterator() = default; -CompartmentLocation CompartmentSetFilteredIterator::operator*() const { - return CompartmentLocation(impl_->operator*().clone()); +const CompartmentLocation& CompartmentSetFilteredIterator::operator*() const { + return impl_->operator*(); +} + +const CompartmentLocation* CompartmentSetFilteredIterator::operator->() const { + return impl_->operator->(); } CompartmentSetFilteredIterator& CompartmentSetFilteredIterator::operator++() { From 452be67de12a72426b6269b83e2cbd4aec317ada Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 16:32:02 +0200 Subject: [PATCH 39/44] format --- include/bbp/sonata/compartment_sets.h | 68 +++++++++++++++------------ src/compartment_sets.cpp | 37 ++++++++------- 2 files changed, 57 insertions(+), 48 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 898ee24..bdd1849 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -24,7 +24,7 @@ class CompartmentSets; */ class SONATA_API CompartmentLocation { -private: + private: uint64_t node_id_ = 0; uint64_t section_index_ = 0; double offset_ = 0.0; @@ -33,29 +33,36 @@ class SONATA_API CompartmentLocation void setSectionIndex(int64_t section_index); void setOffset(double offset); -public: + public: explicit CompartmentLocation(int64_t node_id, int64_t section_index, double offset) { setNodeId(node_id); setSectionIndex(section_index); setOffset(offset); } explicit CompartmentLocation(const nlohmann::json& j); - explicit CompartmentLocation(const std::string& content) : CompartmentLocation(nlohmann::json::parse(content)) {} - explicit CompartmentLocation(const char* content) : CompartmentLocation(std::string(content)) {} + explicit CompartmentLocation(const std::string& content) + : CompartmentLocation(nlohmann::json::parse(content)) { } + explicit CompartmentLocation(const char* content) + : CompartmentLocation(std::string(content)) { } bool operator==(const CompartmentLocation& other) const noexcept { - return node_id_ == other.node_id_ && - section_index_ == other.section_index_ && - offset_ == other.offset_; + return node_id_ == other.node_id_ && section_index_ == other.section_index_ && + offset_ == other.offset_; } bool operator!=(const CompartmentLocation& other) const noexcept { return !(*this == other); } - uint64_t nodeId() const { return node_id_; } - uint64_t sectionIndex() const { return section_index_; } - double offset() const { return offset_; } + uint64_t nodeId() const { + return node_id_; + } + uint64_t sectionIndex() const { + return section_index_; + } + double offset() const { + return offset_; + } /// Convenience function to transform the class in a json object nlohmann::json to_json() const { @@ -70,26 +77,27 @@ class SONATA_API CompartmentLocation class SONATA_API CompartmentSetFilteredIterator { public: - using iterator_category = std::forward_iterator_tag; - using value_type = CompartmentLocation; - using difference_type = std::ptrdiff_t; - using pointer = const CompartmentLocation*; - using reference = const CompartmentLocation&; - - explicit CompartmentSetFilteredIterator(std::unique_ptr impl); - CompartmentSetFilteredIterator(const CompartmentSetFilteredIterator& other); - CompartmentSetFilteredIterator& operator=(const CompartmentSetFilteredIterator& other); - CompartmentSetFilteredIterator(CompartmentSetFilteredIterator&&) noexcept; - CompartmentSetFilteredIterator& operator=(CompartmentSetFilteredIterator&&) noexcept; - ~CompartmentSetFilteredIterator(); - - const CompartmentLocation& operator*() const; - const CompartmentLocation* operator->() const; - - CompartmentSetFilteredIterator& operator++(); // prefix ++ - CompartmentSetFilteredIterator operator++(int); // postfix ++ - bool operator==(const CompartmentSetFilteredIterator& other) const; - bool operator!=(const CompartmentSetFilteredIterator& other) const; + using iterator_category = std::forward_iterator_tag; + using value_type = CompartmentLocation; + using difference_type = std::ptrdiff_t; + using pointer = const CompartmentLocation*; + using reference = const CompartmentLocation&; + + explicit CompartmentSetFilteredIterator( + std::unique_ptr impl); + CompartmentSetFilteredIterator(const CompartmentSetFilteredIterator& other); + CompartmentSetFilteredIterator& operator=(const CompartmentSetFilteredIterator& other); + CompartmentSetFilteredIterator(CompartmentSetFilteredIterator&&) noexcept; + CompartmentSetFilteredIterator& operator=(CompartmentSetFilteredIterator&&) noexcept; + ~CompartmentSetFilteredIterator(); + + const CompartmentLocation& operator*() const; + const CompartmentLocation* operator->() const; + + CompartmentSetFilteredIterator& operator++(); // prefix ++ + CompartmentSetFilteredIterator operator++(int); // postfix ++ + bool operator==(const CompartmentSetFilteredIterator& other) const; + bool operator!=(const CompartmentSetFilteredIterator& other) const; private: std::unique_ptr impl_; diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 3ad18a8..0d90195 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -24,7 +24,8 @@ class CompartmentSetFilteredIterator { using pointer = const value_type*; using difference_type = std::ptrdiff_t; using iterator_category = std::forward_iterator_tag; -private: + + private: base_iterator current_; base_iterator end_; bbp::sonata::Selection selection_; // copied @@ -47,7 +48,9 @@ class CompartmentSetFilteredIterator { skip_to_valid(); } CompartmentSetFilteredIterator(base_iterator end) - : current_(end), end_(end), selection_({}) /* empty selection */ {} + : current_(end) + , end_(end) + , selection_({}) /* empty selection */ { } reference operator*() const { return *current_; @@ -342,21 +345,21 @@ class CompartmentSets // CompartmentLocation public API CompartmentLocation::CompartmentLocation(const nlohmann::json& j) { - if (!j.is_array() || j.size() != 3) { - throw SonataError( - "CompartmentLocation must be an array of exactly 3 elements: [node_id, " - "section_index, " - "offset]"); - } + if (!j.is_array() || j.size() != 3) { + throw SonataError( + "CompartmentLocation must be an array of exactly 3 elements: [node_id, " + "section_index, " + "offset]"); + } - setNodeId(get_int64_or_throw(j[0])); - setSectionIndex(get_int64_or_throw(j[1])); + setNodeId(get_int64_or_throw(j[0])); + setSectionIndex(get_int64_or_throw(j[1])); - if (!j[2].is_number()) { - throw SonataError("Offset (third element) must be a number"); - } - setOffset(j[2].get()); + if (!j[2].is_number()) { + throw SonataError("Offset (third element) must be a number"); } + setOffset(j[2].get()); +} void CompartmentLocation::setNodeId(int64_t node_id) { if (node_id < 0) { @@ -366,15 +369,13 @@ void CompartmentLocation::setNodeId(int64_t node_id) { } void CompartmentLocation::setSectionIndex(int64_t section_index) { if (section_index < 0) { - throw SonataError( - fmt::format("Section index must be non-negative, got {}", section_index)); + throw SonataError(fmt::format("Section index must be non-negative, got {}", section_index)); } section_index_ = static_cast(section_index); } void CompartmentLocation::setOffset(double offset) { if (offset < 0.0 || offset > 1.0) { - throw SonataError( - fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); + throw SonataError(fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); } offset_ = offset; } From 54586c595a727ca174870db438b5ce0bb6875fc5 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 16:51:00 +0200 Subject: [PATCH 40/44] values -> getAllCompartmentSets at -> getCompartmentSet --- include/bbp/sonata/compartment_sets.h | 4 ++-- python/bindings.cpp | 5 ++--- src/compartment_sets.cpp | 12 ++++++------ tests/test_compartment_sets.cpp | 10 +++++----- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index bdd1849..b7ab713 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -175,7 +175,7 @@ class SONATA_API CompartmentSets static CompartmentSets fromFile(const std::string& path); /// Access element by key (throws if not found) - CompartmentSet at(const std::string& key) const; + CompartmentSet getCompartmentSet(const std::string& key) const; /// Number of compartment sets std::size_t size() const; @@ -190,7 +190,7 @@ class SONATA_API CompartmentSets std::vector names() const; /// Get all compartment sets as vector - std::vector values() const; + std::vector getAllCompartmentSets() const; /// Get items (key + compartment set) as vector of pairs std::vector> items() const; diff --git a/python/bindings.cpp b/python/bindings.cpp index 1979688..dc0c7a4 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -643,19 +643,18 @@ PYBIND11_MODULE(_libsonata, m) { py::class_(m, "CompartmentSets") .def(py::init()) .def_static("fromFile", &CompartmentSets::fromFile, py::arg("path")) - .def("at", &CompartmentSets::at, py::arg("key")) .def("__contains__", &CompartmentSets::contains, py::arg("key"), DOC_COMPARTMENTSETS(contains)) .def("names", &CompartmentSets::names) - .def("values", &CompartmentSets::values) + .def("values", &CompartmentSets::getAllCompartmentSets) .def("items", &CompartmentSets::items) .def("toJSON", &CompartmentSets::toJSON, DOC_COMPARTMENTSETS(toJSON)) .def("__eq__", &CompartmentSets::operator==) .def("__ne__", &CompartmentSets::operator!=) .def("__len__", &CompartmentSets::size) - .def("__getitem__", &CompartmentSets::at, py::arg("key"), DOC_COMPARTMENTSET(getitem)) + .def("__getitem__", &CompartmentSets::getCompartmentSet, py::arg("key"), DOC_COMPARTMENTSET(getitem)) .def("__repr__", [](const CompartmentSets& self) { auto items = self.items(); diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 0d90195..fd262e9 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -259,7 +259,7 @@ class CompartmentSets : CompartmentSets(json::parse(content)) {} - std::shared_ptr at(const std::string& key) const { + std::shared_ptr getCompartmentSet(const std::string& key) const { return data_.at(key); } @@ -283,7 +283,7 @@ class CompartmentSets return result; } - std::vector> values() const { + std::vector> getAllCompartmentSets() const { std::vector> result; result.reserve(data_.size()); std::transform(data_.begin(), data_.end(), std::back_inserter(result), @@ -509,8 +509,8 @@ CompartmentSets CompartmentSets::fromFile(const std::string& path) { return detail::CompartmentSets::fromFile(path); } -CompartmentSet CompartmentSets::at(const std::string& key) const { - return CompartmentSet(impl_->at(key)); +CompartmentSet CompartmentSets::getCompartmentSet(const std::string& key) const { + return CompartmentSet(impl_->getCompartmentSet(key)); } // Number of compartment sets @@ -534,8 +534,8 @@ std::vector CompartmentSets::names() const { } // Get all compartment sets as vector -std::vector CompartmentSets::values() const { - const auto vals = impl_->values(); +std::vector CompartmentSets::getAllCompartmentSets() const { + const auto vals = impl_->getAllCompartmentSets(); std::vector result; result.reserve(vals.size()); std::transform(vals.begin(), vals.end(), std::back_inserter(result), diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index 2dc1172..145e837 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -292,10 +292,10 @@ TEST_CASE("CompartmentSets public API") { CHECK(sets.contains("cs0")); CHECK(sets.contains("cs1")); - const auto& cs0 = sets.at("cs0"); + const auto& cs0 = sets.getCompartmentSet("cs0"); CHECK(cs0.empty()); - const auto& cs1 = sets.at("cs1"); + const auto& cs1 = sets.getCompartmentSet("cs1"); CHECK_FALSE(cs1.empty()); } @@ -354,7 +354,7 @@ TEST_CASE("CompartmentSets public API") { SECTION("Throws on missing key") { auto sets = CompartmentSets::fromFile("./data/compartment_sets.json"); - CHECK_THROWS_AS(sets.at("not_there"), std::out_of_range); + CHECK_THROWS_AS(sets.getCompartmentSet("not_there"), std::out_of_range); } SECTION("JSON serialization round-trip") { @@ -372,9 +372,9 @@ TEST_CASE("CompartmentSets public API") { CHECK(keys == std::vector{"cs0", "cs1"}); } - SECTION("Values returns vector of CompartmentSet") { + SECTION("GetAllCompartmentSets returns vector of CompartmentSet") { CompartmentSets sets(json); - CHECK(sets.values() == std::vector{cs0, cs1}); + CHECK(sets.getAllCompartmentSets() == std::vector{cs0, cs1}); } SECTION("Items returns vector of pairs (key, CompartmentSet)") { From 32f07791acaad72812e02724b3c6f223bb7b4398 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Thu, 5 Jun 2025 16:51:28 +0200 Subject: [PATCH 41/44] format --- python/bindings.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/python/bindings.cpp b/python/bindings.cpp index dc0c7a4..a72ce6d 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -654,7 +654,10 @@ PYBIND11_MODULE(_libsonata, m) { .def("__eq__", &CompartmentSets::operator==) .def("__ne__", &CompartmentSets::operator!=) .def("__len__", &CompartmentSets::size) - .def("__getitem__", &CompartmentSets::getCompartmentSet, py::arg("key"), DOC_COMPARTMENTSET(getitem)) + .def("__getitem__", + &CompartmentSets::getCompartmentSet, + py::arg("key"), + DOC_COMPARTMENTSET(getitem)) .def("__repr__", [](const CompartmentSets& self) { auto items = self.items(); From 4ff96a20c47d6ec48dfe73a08b92acd6b88b86c1 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Fri, 6 Jun 2025 11:58:57 +0200 Subject: [PATCH 42/44] simplify CompartmentLocation to plain struct --- include/bbp/sonata/compartment_sets.h | 151 +++++++++++++++++--------- python/bindings.cpp | 32 +++--- python/generated/docstrings.h | 2 - python/tests/test_compartment_sets.py | 41 +++---- src/compartment_sets.cpp | 75 ++++++------- tests/test_compartment_sets.cpp | 102 ++++++----------- 6 files changed, 190 insertions(+), 213 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index b7ab713..cbf3c38 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -11,70 +11,111 @@ class CompartmentSetFilteredIterator; class CompartmentSet; class CompartmentSets; } // namespace detail +// /** +// * CompartmentLocation public API. +// * +// * This class uniquely identifies a compartment by a set of node_id, section_index and offset: +// * +// * - node_id: Global ID of the cell (Neuron) to which the compartment belongs. No +// * overlaps among populations. +// * - section_index: Absolute section index. Progressive index that uniquely identifies the section. +// * There is a mapping between neuron section names (i.e. dend[10]) and this index. +// * - offset: Offset of the compartment along the section. The offset is a value between 0 and 1 +// */ + +// class SONATA_API CompartmentLocation +// { +// private: +// uint64_t node_id_ = 0; +// uint64_t section_index_ = 0; +// double offset_ = 0.0; + +// void setNodeId(int64_t node_id); +// void setSectionIndex(int64_t section_index); +// void setOffset(double offset); + +// public: +// explicit CompartmentLocation(int64_t node_id, int64_t section_index, double offset) { +// setNodeId(node_id); +// setSectionIndex(section_index); +// setOffset(offset); +// } +// explicit CompartmentLocation(const nlohmann::json& j); +// explicit CompartmentLocation(const std::string& content) +// : CompartmentLocation(nlohmann::json::parse(content)) { } +// explicit CompartmentLocation(const char* content) +// : CompartmentLocation(std::string(content)) { } + +// bool operator==(const CompartmentLocation& other) const noexcept { +// return node_id_ == other.node_id_ && section_index_ == other.section_index_ && +// offset_ == other.offset_; +// } + +// bool operator!=(const CompartmentLocation& other) const noexcept { +// return !(*this == other); +// } + +// uint64_t nodeId() const { +// return node_id_; +// } +// uint64_t sectionIndex() const { +// return section_index_; +// } +// double offset() const { +// return offset_; +// } + +// /// Convenience function to transform the class in a json object +// nlohmann::json to_json() const { +// return nlohmann::json::array({node_id_, section_index_, offset_}); +// } + +// /// Convenience function to transform the class in a json-formatted string +// std::string toJSON() const { +// return to_json().dump(); +// } +// }; /** - * CompartmentLocation public API. + * CompartmentLocation. * - * This class uniquely identifies a compartment by a set of node_id, section_index and offset: + * This struct uniquely identifies a compartment by a set of node_id, section_index and offset: * * - node_id: Global ID of the cell (Neuron) to which the compartment belongs. No * overlaps among populations. * - section_index: Absolute section index. Progressive index that uniquely identifies the section. * There is a mapping between neuron section names (i.e. dend[10]) and this index. * - offset: Offset of the compartment along the section. The offset is a value between 0 and 1 + * + * Note: it cannot go inside CompartmentSet because then CompartmentSetFilteredIterator needs the full definition of CompartmentSet and CompartmentSet needs the full definition of CompartmentSetFilteredIterator. */ -class SONATA_API CompartmentLocation -{ - private: - uint64_t node_id_ = 0; - uint64_t section_index_ = 0; - double offset_ = 0.0; - - void setNodeId(int64_t node_id); - void setSectionIndex(int64_t section_index); - void setOffset(double offset); - - public: - explicit CompartmentLocation(int64_t node_id, int64_t section_index, double offset) { - setNodeId(node_id); - setSectionIndex(section_index); - setOffset(offset); - } - explicit CompartmentLocation(const nlohmann::json& j); - explicit CompartmentLocation(const std::string& content) - : CompartmentLocation(nlohmann::json::parse(content)) { } - explicit CompartmentLocation(const char* content) - : CompartmentLocation(std::string(content)) { } - - bool operator==(const CompartmentLocation& other) const noexcept { - return node_id_ == other.node_id_ && section_index_ == other.section_index_ && - offset_ == other.offset_; - } - - bool operator!=(const CompartmentLocation& other) const noexcept { - return !(*this == other); - } - - uint64_t nodeId() const { - return node_id_; - } - uint64_t sectionIndex() const { - return section_index_; - } - double offset() const { - return offset_; - } - - /// Convenience function to transform the class in a json object - nlohmann::json to_json() const { - return nlohmann::json::array({node_id_, section_index_, offset_}); - } - - /// Convenience function to transform the class in a json-formatted string - std::string toJSON() const { - return to_json().dump(); - } +struct CompartmentLocation { + public: + uint64_t nodeId = 0; + uint64_t sectionIndex = 0; + double offset = 0.0; + + /// Comparator. Used to compare vectors in CompartmentSet. More idiomatic than defining a comaprator on the fly + bool operator==(const CompartmentLocation& other) const { + return nodeId == other.nodeId && + sectionIndex == other.sectionIndex && + offset == other.offset; + } + + bool operator!=(const CompartmentLocation& other) const { + return !(*this == other); + } }; +/// Ostream << operator used by catch2 when there are problems for example +inline std::ostream& operator<<(std::ostream& os, const CompartmentLocation& cl) { + os << "CompartmentLocation(" + << "nodeId: " << cl.nodeId << ", " + << "sectionIndex: " << cl.sectionIndex << ", " + << "offset: " << cl.offset + << ")"; + return os; +} + class SONATA_API CompartmentSetFilteredIterator { public: using iterator_category = std::forward_iterator_tag; @@ -103,6 +144,7 @@ class SONATA_API CompartmentSetFilteredIterator { std::unique_ptr impl_; }; + /** * CompartmentSet public API. * @@ -114,6 +156,8 @@ class SONATA_API CompartmentSet { public: + + CompartmentSet() = delete; explicit CompartmentSet(const std::string& json_content); @@ -148,6 +192,7 @@ class SONATA_API CompartmentSet std::shared_ptr impl_; }; + /** * @class CompartmentSets * @brief A container class that manages a collection of named CompartmentSet objects. diff --git a/python/bindings.cpp b/python/bindings.cpp index a72ce6d..a20637b 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -549,32 +549,26 @@ PYBIND11_MODULE(_libsonata, m) { .def("toJSON", &NodeSets::toJSON, DOC_NODESETS(toJSON)); py::class_(m, "CompartmentLocation") - .def(py::init()) - .def(py::init(), - py::arg("node_id"), - py::arg("section_index"), - py::arg("offset")) - .def_property_readonly("node_id", - &CompartmentLocation::nodeId, - DOC_COMPARTMENTLOCATION(nodeId)) - .def_property_readonly("section_index", - &CompartmentLocation::sectionIndex, - DOC_COMPARTMENTLOCATION(sectionIndex)) - .def_property_readonly("offset", - &CompartmentLocation::offset, - DOC_COMPARTMENTLOCATION(offset)) - .def("toJSON", &CompartmentLocation::toJSON, DOC_COMPARTMENTLOCATION(toJSON)) .def("__eq__", &CompartmentLocation::operator==) .def("__ne__", &CompartmentLocation::operator!=) .def("__repr__", [](const CompartmentLocation& self) { return fmt::format("CompartmentLocation({}, {}, {})", - self.nodeId(), - self.sectionIndex(), - self.offset()); + self.nodeId, + self.sectionIndex, + self.offset); }) .def("__str__", - [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); }); + [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); }) + .def_readonly("node_id", + &CompartmentLocation::nodeId, + DOC_COMPARTMENTLOCATION(nodeId)) + .def_readonly("section_index", + &CompartmentLocation::sectionIndex, + DOC_COMPARTMENTLOCATION(sectionIndex)) + .def_readonly("offset", + &CompartmentLocation::offset, + DOC_COMPARTMENTLOCATION(offset)); py::class_(m, "CompartmentSet") .def(py::init()) diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h index 014420f..b16bb2b 100644 --- a/python/generated/docstrings.h +++ b/python/generated/docstrings.h @@ -387,8 +387,6 @@ static const char *__doc_bbp_sonata_CompartmentLocation_sectionIndex = R"doc(Abs static const char *__doc_bbp_sonata_CompartmentLocation_offset = R"doc(Offset of the compartment along the section)doc"; -static const char *__doc_bbp_sonata_CompartmentLocation_toJSON = R"doc(Return the compartment set element as a JSON string.)doc"; - static const char *__doc_bbp_sonata_CompartmentSet_population = R"doc(Population name)doc"; static const char *__doc_bbp_sonata_CompartmentSet_toJSON = R"doc(Return the compartment set as a JSON string.)doc"; diff --git a/python/tests/test_compartment_sets.py b/python/tests/test_compartment_sets.py index 8194a07..d7e2ecf 100644 --- a/python/tests/test_compartment_sets.py +++ b/python/tests/test_compartment_sets.py @@ -11,45 +11,32 @@ PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../tests/data') - class TestCompartmentLocation(unittest.TestCase): + def setUp(self): + self.json = '''{ + "population": "pop0", + "compartment_set": [ + [4, 40, 0.9], + [4, 40, 0.9], + [3, 30, 0.75] + ] + }''' + self.cs = CompartmentSet(self.json) def test_constructor_from_values(self): - loc = CompartmentLocation(4, 40, 0.9) - self.assertEqual(loc.node_id, 4) - self.assertEqual(loc.section_index, 40) - self.assertAlmostEqual(loc.offset, 0.9) - - def test_constructor_from_string(self): - loc = CompartmentLocation("[4, 40, 0.9]") + loc = self.cs[0] self.assertEqual(loc.node_id, 4) self.assertEqual(loc.section_index, 40) self.assertAlmostEqual(loc.offset, 0.9) - def test_toJSON(self): - loc = CompartmentLocation(4, 40, 0.9) - self.assertEqual(loc.toJSON(), "[4,40,0.9]") - def test_equality(self): - loc1 = CompartmentLocation(4, 40, 0.9) - loc2 = CompartmentLocation("[4, 40, 0.9]") - loc3 = CompartmentLocation(5, 40, 0.9) - self.assertEqual(loc1, loc2) - self.assertNotEqual(loc1, loc3) + self.assertEqual(self.cs[0], self.cs[1]) + self.assertNotEqual(self.cs[0], self.cs[2]) def test_repr_and_str(self): - loc = CompartmentLocation(4, 40, 0.9) + loc = self.cs[0] expected = "CompartmentLocation(4, 40, 0.9)" self.assertEqual(repr(loc), expected) self.assertEqual(str(loc), repr(loc)) # str should delegate to repr - - def test_assignment_creates_copy(self): - loc1 = CompartmentLocation(1, 2, 0.3) - loc2 = loc1 # This is a reference assignment in Python - self.assertEqual(loc1, loc2) - - # Now mutate loc1 and check if loc2 is affected — which it will be, unless toJSON etc. are implemented with deep semantics - self.assertIs(loc1, loc2) # They reference the same object - class TestCompartmentSet(unittest.TestCase): def setUp(self): self.json = '''{ diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index fd262e9..74c478e 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -16,6 +16,9 @@ namespace detail { using json = nlohmann::json; + + + class CompartmentSetFilteredIterator { public: using base_iterator = std::vector::const_iterator; @@ -32,7 +35,7 @@ class CompartmentSetFilteredIterator { void skip_to_valid() { while (current_ != end_) { - if (selection_.empty() || selection_.contains(current_->nodeId())) { + if (selection_.empty() || selection_.contains(current_->nodeId)) { break; } ++current_; @@ -84,6 +87,7 @@ class CompartmentSetFilteredIterator { return std::make_unique(*this); } }; + class CompartmentSet { public: using container_t = std::vector; @@ -101,6 +105,27 @@ class CompartmentSet { CompartmentSet(const CompartmentSet& other) = default; CompartmentSet(const std::string& population, const container_t& compartment_locations): population_(population), compartment_locations_(compartment_locations) {} + static CompartmentLocation _parseCompartmentLocation(const nlohmann::json& j) { + if (!j.is_array() || j.size() != 3) { + throw SonataError( + "CompartmentLocation must be an array of exactly 3 elements: [node_id, " + "section_index, " + "offset]"); + } + + const uint64_t node_id = get_uint64_or_throw(j[0]); + const uint64_t section_index = get_uint64_or_throw(j[1]); + if (!j[2].is_number()) { + throw SonataError("Offset (third element) must be a number"); + } + const double offset = j[2].get(); + if (offset < 0.0 || offset > 1.0) { + throw SonataError(fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); + } + + return {node_id, section_index, offset}; + } + public: // Construct from JSON string (delegates to JSON constructor) @@ -126,7 +151,7 @@ class CompartmentSet { compartment_locations_.reserve(comp_it->size()); for (auto&& el : *comp_it) { - compartment_locations_.emplace_back(std::forward(el)); + compartment_locations_.emplace_back(CompartmentSet::_parseCompartmentLocation(el)); } compartment_locations_.shrink_to_fit(); } @@ -154,7 +179,7 @@ class CompartmentSet { return static_cast(std::count_if(compartment_locations_.begin(), compartment_locations_.end(), [&](const CompartmentLocation& loc) { - return selection.contains(loc.nodeId()); + return selection.contains(loc.nodeId); })); } @@ -172,7 +197,7 @@ class CompartmentSet { result.reserve(compartment_locations_.size()); for (const auto& elem : compartment_locations_) { - uint64_t id = elem.nodeId(); + uint64_t id = elem.nodeId; if (seen.insert(id).second) { // insert returns {iterator, bool} result.push_back(id); } @@ -191,7 +216,7 @@ class CompartmentSet { j["compartment_set"] = nlohmann::json::array(); for (const auto& elem : compartment_locations_) { - j["compartment_set"].push_back(elem.to_json()); + j["compartment_set"].push_back(nlohmann::json::array({elem.nodeId, elem.sectionIndex, elem.offset})); } return j; @@ -208,7 +233,7 @@ class CompartmentSet { std::vector filtered; filtered.reserve(compartment_locations_.size()); for (const auto& el : compartment_locations_) { - if (selection.contains(el.nodeId())) { + if (selection.contains(el.nodeId)) { filtered.emplace_back(el); } } @@ -342,44 +367,6 @@ class CompartmentSets } // namespace detail -// CompartmentLocation public API - -CompartmentLocation::CompartmentLocation(const nlohmann::json& j) { - if (!j.is_array() || j.size() != 3) { - throw SonataError( - "CompartmentLocation must be an array of exactly 3 elements: [node_id, " - "section_index, " - "offset]"); - } - - setNodeId(get_int64_or_throw(j[0])); - setSectionIndex(get_int64_or_throw(j[1])); - - if (!j[2].is_number()) { - throw SonataError("Offset (third element) must be a number"); - } - setOffset(j[2].get()); -} - -void CompartmentLocation::setNodeId(int64_t node_id) { - if (node_id < 0) { - throw SonataError(fmt::format("Node id must be non-negative, got {}", node_id)); - } - node_id_ = static_cast(node_id); -} -void CompartmentLocation::setSectionIndex(int64_t section_index) { - if (section_index < 0) { - throw SonataError(fmt::format("Section index must be non-negative, got {}", section_index)); - } - section_index_ = static_cast(section_index); -} -void CompartmentLocation::setOffset(double offset) { - if (offset < 0.0 || offset > 1.0) { - throw SonataError(fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); - } - offset_ = offset; -} - // CompartmentSetFilteredIterator public API // Constructor diff --git a/tests/test_compartment_sets.cpp b/tests/test_compartment_sets.cpp index 145e837..7fc37e1 100644 --- a/tests/test_compartment_sets.cpp +++ b/tests/test_compartment_sets.cpp @@ -9,80 +9,46 @@ using json = nlohmann::json; TEST_CASE("CompartmentLocation public API") { - SECTION("Construct from valid nodeId, section_idx, offset") { - CompartmentLocation loc(1, 10, 0.5); - REQUIRE(loc.nodeId() == 1); - REQUIRE(loc.sectionIndex() == 10); - REQUIRE(loc.offset() == Approx(0.5)); - } + std::string json_content = R"( + { + "population": "test_population", + "compartment_set": [ + [1, 10, 0.5] + ] + } + )"; + CompartmentSet cs(json_content); - SECTION("Construct from valid JSON string") { - std::string json_str = "[1, 10, 0.5]"; - CompartmentLocation loc(json_str); - REQUIRE(loc.nodeId() == 1); - REQUIRE(loc.sectionIndex() == 10); - REQUIRE(loc.offset() == Approx(0.5)); + SECTION("Construct from valid nodeId, section_idx, offset") { + const auto& loc = cs[0]; + REQUIRE(loc.nodeId == 1); + REQUIRE(loc.sectionIndex == 10); + REQUIRE(loc.offset == Approx(0.5)); + REQUIRE(cs[0] == CompartmentLocation{1, 10, 0.5}); } SECTION("Invalid JSON string throws") { - REQUIRE_THROWS_AS(CompartmentLocation("{\"nodeId\": 1}"), SonataError); - REQUIRE_THROWS_AS(CompartmentLocation("[1, 2]"), SonataError); - REQUIRE_THROWS_AS(CompartmentLocation("[1, 2, 0.1, 1]"), SonataError); - REQUIRE_THROWS_AS(CompartmentLocation("[\"a\", 2, 0.5]"), SonataError); - REQUIRE_THROWS_AS(CompartmentLocation("[1, \"a\", 0.5]"), SonataError); - REQUIRE_THROWS_AS(CompartmentLocation("[1, 2, \"a\"]"), SonataError); - REQUIRE_THROWS_AS(CompartmentLocation("[1, 2, 2.0]"), SonataError); - REQUIRE_THROWS_AS(CompartmentLocation("[1, 2, -0.1]"), SonataError); - REQUIRE_THROWS_AS(CompartmentLocation("[-1, 2, 0.1]"), SonataError); - REQUIRE_THROWS_AS(CompartmentLocation("[1, -2, 0.1]"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ ["bla", 2, 0.1] ]})"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ [1, 2] ]})"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ [1, 2, 0.1, 1] ]})"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ ["a", 2, 0.5] ]})"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ [1, "a", 0.5] ]})"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ [1, 2, "a"] ]})"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ [1, 2, 2.0] ]})"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ [1, 2, -0.1] ]})"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ [-1, 2, 0.1] ]})"), SonataError); + REQUIRE_THROWS_AS(CompartmentSet(R"({"population": "pop0", "compartment_set": [ [1, -2, 0.1] ]})"), SonataError); } SECTION("Equality operators") { - CompartmentLocation loc1(1, 10, 0.5); - CompartmentLocation loc2(1, 10, 0.5); - CompartmentLocation loc3(1, 10, 0.6); + CompartmentLocation loc1{1, 10, 0.5}; + CompartmentLocation loc2{1, 10, 0.5}; + CompartmentLocation loc3{1, 10, 0.6}; REQUIRE(loc1 == loc2); REQUIRE_FALSE(loc1 != loc2); REQUIRE(loc1 != loc3); } - - SECTION("Copy constructor and assignment") { - CompartmentLocation original(2, 20, 0.7); - CompartmentLocation copy_constructed(original); - CompartmentLocation copy_assigned(0, 0, 0); - copy_assigned = original; - - REQUIRE(copy_constructed == original); - REQUIRE(copy_assigned == original); - } - - SECTION("Move constructor and assignment") { - CompartmentLocation original(3, 30, 0.8); - CompartmentLocation moved_constructed(std::move(original)); - - REQUIRE(moved_constructed.nodeId() == 3); - REQUIRE(moved_constructed.sectionIndex() == 30); - REQUIRE(moved_constructed.offset() == Approx(0.8)); - - CompartmentLocation another(0, 0, 0); - another = std::move(moved_constructed); - - REQUIRE(another.nodeId() == 3); - REQUIRE(another.sectionIndex() == 30); - REQUIRE(another.offset() == Approx(0.8)); - } - - SECTION("toJSON returns valid JSON string") { - CompartmentLocation loc(4, 40, 0.9); - auto json_str = loc.toJSON(); - - auto json = nlohmann::json::parse(json_str); - REQUIRE(json.is_array()); - REQUIRE(json[0] == 4); - REQUIRE(json[1] == 40); - REQUIRE(json[2] == Approx(0.9)); - } } @@ -108,10 +74,10 @@ TEST_CASE("CompartmentSet public API") { REQUIRE(cs.size() == 4); // Access elements by index - REQUIRE(cs[0] == CompartmentLocation(1, 10, 0.5)); - REQUIRE(cs[1] == CompartmentLocation(2, 20, 0.25)); - REQUIRE(cs[2] == CompartmentLocation(3, 30, 0.75)); - REQUIRE(cs[3] == CompartmentLocation(2, 20, 0.25)); + REQUIRE(cs[0] == CompartmentLocation{1, 10, 0.5}); + REQUIRE(cs[1] == CompartmentLocation{2, 20, 0.25}); + REQUIRE(cs[2] == CompartmentLocation{3, 30, 0.75}); + REQUIRE(cs[3] == CompartmentLocation{2, 20, 0.25}); REQUIRE_THROWS_AS(cs[4], std::out_of_range); REQUIRE(cs.toJSON() == json::parse(json_content).dump()); } @@ -161,14 +127,14 @@ TEST_CASE("CompartmentSet public API") { std::vector nodeIds; for (auto it = pp.first; it != pp.second; ++it) { - nodeIds.push_back((*it).nodeId()); + nodeIds.push_back((*it).nodeId); } REQUIRE(nodeIds.size() == 4); REQUIRE((nodeIds == std::vector{1, 2, 3, 2})); nodeIds.clear(); for (auto it = cs.filtered_crange(Selection::fromValues({2, 3})).first; it != pp.second; ++it) { - nodeIds.push_back((*it).nodeId()); + nodeIds.push_back((*it).nodeId); } REQUIRE(nodeIds.size() == 3); REQUIRE((nodeIds == std::vector{2, 3, 2})); From bb0353cc183252295ed83ec27234993102365609 Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Fri, 6 Jun 2025 11:59:27 +0200 Subject: [PATCH 43/44] format --- include/bbp/sonata/compartment_sets.h | 45 +++++++++++++-------------- python/bindings.cpp | 12 +++---- src/compartment_sets.cpp | 8 ++--- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index cbf3c38..416ff09 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -18,7 +18,8 @@ class CompartmentSets; // * // * - node_id: Global ID of the cell (Neuron) to which the compartment belongs. No // * overlaps among populations. -// * - section_index: Absolute section index. Progressive index that uniquely identifies the section. +// * - section_index: Absolute section index. Progressive index that uniquely identifies the +// section. // * There is a mapping between neuron section names (i.e. dend[10]) and this index. // * - offset: Offset of the compartment along the section. The offset is a value between 0 and 1 // */ @@ -85,25 +86,27 @@ class CompartmentSets; * - section_index: Absolute section index. Progressive index that uniquely identifies the section. * There is a mapping between neuron section names (i.e. dend[10]) and this index. * - offset: Offset of the compartment along the section. The offset is a value between 0 and 1 - * - * Note: it cannot go inside CompartmentSet because then CompartmentSetFilteredIterator needs the full definition of CompartmentSet and CompartmentSet needs the full definition of CompartmentSetFilteredIterator. + * + * Note: it cannot go inside CompartmentSet because then CompartmentSetFilteredIterator needs the + * full definition of CompartmentSet and CompartmentSet needs the full definition of + * CompartmentSetFilteredIterator. */ struct CompartmentLocation { - public: - uint64_t nodeId = 0; - uint64_t sectionIndex = 0; - double offset = 0.0; - - /// Comparator. Used to compare vectors in CompartmentSet. More idiomatic than defining a comaprator on the fly - bool operator==(const CompartmentLocation& other) const { - return nodeId == other.nodeId && - sectionIndex == other.sectionIndex && - offset == other.offset; - } - - bool operator!=(const CompartmentLocation& other) const { - return !(*this == other); - } + public: + uint64_t nodeId = 0; + uint64_t sectionIndex = 0; + double offset = 0.0; + + /// Comparator. Used to compare vectors in CompartmentSet. More idiomatic than defining a + /// comaprator on the fly + bool operator==(const CompartmentLocation& other) const { + return nodeId == other.nodeId && sectionIndex == other.sectionIndex && + offset == other.offset; + } + + bool operator!=(const CompartmentLocation& other) const { + return !(*this == other); + } }; /// Ostream << operator used by catch2 when there are problems for example @@ -111,8 +114,7 @@ inline std::ostream& operator<<(std::ostream& os, const CompartmentLocation& cl) os << "CompartmentLocation(" << "nodeId: " << cl.nodeId << ", " << "sectionIndex: " << cl.sectionIndex << ", " - << "offset: " << cl.offset - << ")"; + << "offset: " << cl.offset << ")"; return os; } @@ -155,9 +157,6 @@ class SONATA_API CompartmentSetFilteredIterator { class SONATA_API CompartmentSet { public: - - - CompartmentSet() = delete; explicit CompartmentSet(const std::string& json_content); diff --git a/python/bindings.cpp b/python/bindings.cpp index a20637b..1bc00c1 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -560,15 +560,11 @@ PYBIND11_MODULE(_libsonata, m) { }) .def("__str__", [](const CompartmentLocation& self) { return py::str(py::repr(py::cast(self))); }) - .def_readonly("node_id", - &CompartmentLocation::nodeId, - DOC_COMPARTMENTLOCATION(nodeId)) + .def_readonly("node_id", &CompartmentLocation::nodeId, DOC_COMPARTMENTLOCATION(nodeId)) .def_readonly("section_index", - &CompartmentLocation::sectionIndex, - DOC_COMPARTMENTLOCATION(sectionIndex)) - .def_readonly("offset", - &CompartmentLocation::offset, - DOC_COMPARTMENTLOCATION(offset)); + &CompartmentLocation::sectionIndex, + DOC_COMPARTMENTLOCATION(sectionIndex)) + .def_readonly("offset", &CompartmentLocation::offset, DOC_COMPARTMENTLOCATION(offset)); py::class_(m, "CompartmentSet") .def(py::init()) diff --git a/src/compartment_sets.cpp b/src/compartment_sets.cpp index 74c478e..6f2f283 100644 --- a/src/compartment_sets.cpp +++ b/src/compartment_sets.cpp @@ -17,8 +17,6 @@ namespace detail { using json = nlohmann::json; - - class CompartmentSetFilteredIterator { public: using base_iterator = std::vector::const_iterator; @@ -120,7 +118,8 @@ class CompartmentSet { } const double offset = j[2].get(); if (offset < 0.0 || offset > 1.0) { - throw SonataError(fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); + throw SonataError( + fmt::format("Offset must be between 0 and 1 inclusive, got {}", offset)); } return {node_id, section_index, offset}; @@ -216,7 +215,8 @@ class CompartmentSet { j["compartment_set"] = nlohmann::json::array(); for (const auto& elem : compartment_locations_) { - j["compartment_set"].push_back(nlohmann::json::array({elem.nodeId, elem.sectionIndex, elem.offset})); + j["compartment_set"].push_back( + nlohmann::json::array({elem.nodeId, elem.sectionIndex, elem.offset})); } return j; From 6208c8e9e26b556b63b9d1c2353525455816f3db Mon Sep 17 00:00:00 2001 From: Alessandro Cattabiani Date: Fri, 6 Jun 2025 13:40:20 +0200 Subject: [PATCH 44/44] remove commented code --- include/bbp/sonata/compartment_sets.h | 66 +-------------------------- 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/include/bbp/sonata/compartment_sets.h b/include/bbp/sonata/compartment_sets.h index 416ff09..802aab5 100644 --- a/include/bbp/sonata/compartment_sets.h +++ b/include/bbp/sonata/compartment_sets.h @@ -11,71 +11,7 @@ class CompartmentSetFilteredIterator; class CompartmentSet; class CompartmentSets; } // namespace detail -// /** -// * CompartmentLocation public API. -// * -// * This class uniquely identifies a compartment by a set of node_id, section_index and offset: -// * -// * - node_id: Global ID of the cell (Neuron) to which the compartment belongs. No -// * overlaps among populations. -// * - section_index: Absolute section index. Progressive index that uniquely identifies the -// section. -// * There is a mapping between neuron section names (i.e. dend[10]) and this index. -// * - offset: Offset of the compartment along the section. The offset is a value between 0 and 1 -// */ - -// class SONATA_API CompartmentLocation -// { -// private: -// uint64_t node_id_ = 0; -// uint64_t section_index_ = 0; -// double offset_ = 0.0; - -// void setNodeId(int64_t node_id); -// void setSectionIndex(int64_t section_index); -// void setOffset(double offset); - -// public: -// explicit CompartmentLocation(int64_t node_id, int64_t section_index, double offset) { -// setNodeId(node_id); -// setSectionIndex(section_index); -// setOffset(offset); -// } -// explicit CompartmentLocation(const nlohmann::json& j); -// explicit CompartmentLocation(const std::string& content) -// : CompartmentLocation(nlohmann::json::parse(content)) { } -// explicit CompartmentLocation(const char* content) -// : CompartmentLocation(std::string(content)) { } - -// bool operator==(const CompartmentLocation& other) const noexcept { -// return node_id_ == other.node_id_ && section_index_ == other.section_index_ && -// offset_ == other.offset_; -// } - -// bool operator!=(const CompartmentLocation& other) const noexcept { -// return !(*this == other); -// } - -// uint64_t nodeId() const { -// return node_id_; -// } -// uint64_t sectionIndex() const { -// return section_index_; -// } -// double offset() const { -// return offset_; -// } - -// /// Convenience function to transform the class in a json object -// nlohmann::json to_json() const { -// return nlohmann::json::array({node_id_, section_index_, offset_}); -// } - -// /// Convenience function to transform the class in a json-formatted string -// std::string toJSON() const { -// return to_json().dump(); -// } -// }; + /** * CompartmentLocation. *