Skip to content

Commit 54ebb00

Browse files
authored
Replace lfp_factors unordered_map with flat arrays (#3781)
Replace `CellMapping::lfp_factors` (`std::unordered_map<int, std::vector<double>>`) with two flat contiguous vectors: - `lfp_segment_ids`: segment IDs (one per compartment) - `lfp_factors_flat`: all electrode weights in a single allocation (stride = num_electrodes) Additionally: - `num_electrodes()` is now derived from the arrays (`lfp_factors_flat.size() / lfp_segment_ids.size()`) — no stored member needed - `add_segment_lfp_factor()` validates stride consistency on every insertion (mismatched factor count → abort with clear error) - Renamed `to_report` → `electrode_outputs` in `lfp_calc` for clarity ## Motivation The unordered_map was never used for random O(1) lookup in the hot path — `lfp_calc` iterates ALL entries sequentially. The hash map introduces: - ~84 bytes overhead per entry (bucket + inner vector header + heap allocation) - Pointer chasing and scattered memory in the hot path - ~3x more memory than needed The flat layout matches the actual access pattern: sequential iteration with a constant stride. ## Changes - `nrnsection_mapping.hpp`: - Replace `unordered_map<int, vector<double>>` with `vector<int>` + `vector<double>` - `num_electrodes()` returns `size_t`, derived from array sizes - `add_segment_lfp_factor()` validates stride consistency, returns early on empty factors - `report_event.cpp`: - `lfp_calc` uses index-based iteration over flat arrays - Uses `size_t` for loop indices, `const auto` for locals - Renamed `to_report` → `electrode_outputs` - `test/coreneuron/unit/lfp/lfp.cpp`: Update assertion to use `lfp_segment_ids.size()` ## Testing - nrn `ctest -R lfp` passes - neurodamus `tox -m local` passes (all envs including scientific LFP tests) - No behavioral change — purely internal data structure swap - The `add_segment_lfp_factor` API is unchanged (same signature), so no changes needed in callers
1 parent fd201de commit 54ebb00

5 files changed

Lines changed: 59 additions & 42 deletions

File tree

src/coreneuron/io/nrn_filehandler.hpp

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,20 +144,16 @@ class FileHandler {
144144
lfp_factors = read_vector<double>(total_lfp_factors);
145145
}
146146

147-
int factor_offset = 0;
148147
for (int i = 0; i < nseg; i++) {
149148
mapinfo->add_segment(sec[i], seg[i]);
150149
ntmapping->add_segment_id(seg[i]);
151-
int factor_offset = i * num_electrodes;
152150
if (total_lfp_factors > 0) {
153151
// Abort if the factors contains a NaN
154152
nrn_assert(count_if(lfp_factors.begin(), lfp_factors.end(), [](double d) {
155153
return std::isnan(d);
156154
}) == 0);
157-
std::vector<double> segment_factors(lfp_factors.begin() + factor_offset,
158-
lfp_factors.begin() + factor_offset +
159-
num_electrodes);
160-
cmap->add_segment_lfp_factor(seg[i], segment_factors);
155+
auto begin = lfp_factors.begin() + i * num_electrodes;
156+
cmap->add_segment_lfp_factor(seg[i], begin, begin + num_electrodes);
161157
}
162158
}
163159
}

src/coreneuron/io/nrnsection_mapping.hpp

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <vector>
1919

2020
#include "coreneuron/io/reports/nrnreport.hpp"
21+
#include "coreneuron/utils/nrn_assert.h"
2122
#include "coreneuron/utils/utils.hpp"
2223

2324
namespace coreneuron {
@@ -73,8 +74,11 @@ struct CellMapping {
7374
/** list of section lists (like soma, axon, apic) */
7475
std::vector<std::shared_ptr<SecMapping>> sec_mappings;
7576

76-
/** map containing segment ids an its respective lfp factors */
77-
std::unordered_map<int, std::vector<double>> lfp_factors;
77+
/** segment ids for lfp factors (one per compartment with LFP data) */
78+
std::vector<int> lfp_segment_ids;
79+
80+
/** flat array of lfp factors: stride = num_electrodes, indexed as [i * stride + e] */
81+
std::vector<double> lfp_factors_flat;
7882

7983
CellMapping(int g)
8084
: gid(g) {}
@@ -99,13 +103,9 @@ struct CellMapping {
99103
});
100104
}
101105

102-
/** @brief return the number of electrodes in the lfp_factors map **/
103-
int num_electrodes() const {
104-
int num_electrodes = 0;
105-
if (!lfp_factors.empty()) {
106-
num_electrodes = lfp_factors.begin()->second.size();
107-
}
108-
return num_electrodes;
106+
/** @brief return the number of electrodes per segment **/
107+
size_t num_electrodes() const {
108+
return lfp_segment_ids.empty() ? 0 : lfp_factors_flat.size() / lfp_segment_ids.size();
109109
}
110110

111111
/** @brief number of section lists */
@@ -148,8 +148,18 @@ struct CellMapping {
148148
}
149149

150150
/** @brief add the lfp electrode factors of a segment_id */
151-
void add_segment_lfp_factor(const int segment_id, std::vector<double>& factors) {
152-
lfp_factors.insert({segment_id, factors});
151+
void add_segment_lfp_factor(const int segment_id,
152+
std::vector<double>::const_iterator begin,
153+
std::vector<double>::const_iterator end) {
154+
const size_t n = std::distance(begin, end);
155+
if (n == 0) {
156+
return;
157+
}
158+
const auto curr_n_electrodes = num_electrodes();
159+
// All segments must have the same number of electrode factors
160+
nrn_assert(curr_n_electrodes == 0 || n == curr_n_electrodes);
161+
lfp_segment_ids.push_back(segment_id);
162+
lfp_factors_flat.insert(lfp_factors_flat.end(), begin, end);
153163
}
154164
};
155165

src/coreneuron/io/phase3.cpp

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,13 @@ void Phase3::read_direct(NrnThreadMappingInfo* ntmapping, const NrnThread& nt) {
8383
for (int i_seg = 0; i_seg < n_seg; i_seg++) {
8484
smap->add_segment(data_sec[i_seg], data_seg[i_seg]);
8585
ntmapping->add_segment_id(data_seg[i_seg]);
86-
int factor_offset = i_seg * n_electrodes;
8786
if (total_lfp_factors > 0) {
8887
// Abort if the factors contains a NaN
8988
nrn_assert(count_if(data_lfp.begin(), data_lfp.end(), [](double d) {
9089
return std::isnan(d);
9190
}) == 0);
92-
std::vector<double> segment_factors(data_lfp.begin() + factor_offset,
93-
data_lfp.begin() + factor_offset +
94-
n_electrodes);
95-
cmap->add_segment_lfp_factor(data_seg[i_seg], segment_factors);
91+
auto begin = data_lfp.begin() + i_seg * n_electrodes;
92+
cmap->add_segment_lfp_factor(data_seg[i_seg], begin, begin + n_electrodes);
9693
}
9794
}
9895
cmap->add_sec_map(smap);

src/coreneuron/io/reports/report_event.cpp

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
*/
88

99
#include "report_event.hpp"
10+
11+
#include <numeric>
12+
1013
#include "coreneuron/sim/multicore.hpp"
1114
#include "coreneuron/io/reports/nrnreport.hpp"
1215
#include "coreneuron/utils/nrn_assert.h"
@@ -77,33 +80,44 @@ void ReportEvent::summation_alu(NrnThread* nt) {
7780
}
7881
}
7982

83+
/** @brief Compute Local Field Potentials (LFP) for each cell.
84+
*
85+
* For every segment of a cell, computes the total membrane current (imem + iclamp)
86+
* and accumulates the weighted contribution to each electrode using precomputed
87+
* transfer factors. Results are written into the report output buffers.
88+
*/
8089
void ReportEvent::lfp_calc(NrnThread* nt) {
8190
auto* mapinfo = static_cast<NrnThreadMappingInfo*>(nt->mapping);
8291
double* fast_imem_rhs = nt->nrn_fast_imem->nrn_sav_rhs;
8392
auto& summation_report = nt->summation_report_handler_->summation_reports_[report_path];
8493
for (const auto& kv: vars_to_report) {
8594
int gid = kv.first;
86-
const auto& to_report = kv.second;
95+
const auto& electrode_outputs = kv.second;
8796
const auto& cell_mapping = mapinfo->get_cell_mapping(gid);
88-
int num_electrodes = cell_mapping->num_electrodes();
89-
std::vector<double> lfp_values(num_electrodes, 0.0);
90-
for (const auto& kv: cell_mapping->lfp_factors) {
91-
int segment_id = kv.first;
92-
const auto& factors = kv.second;
93-
int electrode_id = 0;
94-
for (const auto& factor: factors) {
95-
double iclamp = 0.0;
96-
for (const auto& value: summation_report.currents_[segment_id]) {
97-
double current_value = *value.first;
98-
int scale = value.second;
99-
iclamp += current_value * scale;
100-
}
101-
lfp_values[electrode_id] += (fast_imem_rhs[segment_id] + iclamp) * factor;
102-
electrode_id++;
97+
const auto n_electrodes = cell_mapping->num_electrodes();
98+
const auto n_segments = cell_mapping->lfp_segment_ids.size();
99+
std::vector<double> lfp_values(n_electrodes, 0.0);
100+
for (size_t i = 0; i < n_segments; i++) {
101+
const auto segment_id = cell_mapping->lfp_segment_ids[i];
102+
103+
// compute imem + iclamp
104+
const double imem = std::accumulate(summation_report.currents_[segment_id].begin(),
105+
summation_report.currents_[segment_id].end(),
106+
fast_imem_rhs[segment_id],
107+
[](double sum, const auto& value) {
108+
return sum + *value.first * value.second;
109+
});
110+
111+
// dot product with the factors
112+
const double* factors = &cell_mapping->lfp_factors_flat[i * n_electrodes];
113+
for (size_t e = 0; e < n_electrodes; e++) {
114+
lfp_values[e] += imem * factors[e];
103115
}
104116
}
105-
for (int i = 0; i < to_report.size(); i++) {
106-
*(to_report[i].var_value) = lfp_values[i];
117+
118+
// write LFP values to report output buffers
119+
for (size_t e = 0; e < electrode_outputs.size(); e++) {
120+
*(electrode_outputs[e].var_value) = lfp_values[e];
107121
}
108122
}
109123
}

test/coreneuron/unit/lfp/lfp.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,15 +131,15 @@ TEST_CASE("LFP_ReportEvent") {
131131
mapinfo->add_cell_mapping(cmap);
132132
for (const auto& segment: segment_ids) {
133133
std::vector<double> lfp_factors{segment + 1.0, segment + 2.0};
134-
cmap->add_segment_lfp_factor(segment, lfp_factors);
134+
cmap->add_segment_lfp_factor(segment, lfp_factors.begin(), lfp_factors.end());
135135
}
136136
}
137137
mapinfo->prepare_lfp();
138138
// Total number of electrodes 2 gids * 2 factors
139139

140140
auto c42 = mapinfo->get_cell_mapping(42);
141141
auto c134 = mapinfo->get_cell_mapping(134);
142-
REQUIRE(c42->lfp_factors.size() == 5);
142+
REQUIRE(c42->lfp_segment_ids.size() == 5);
143143
REQUIRE(c134->num_electrodes() == 2);
144144

145145
// Pass _lfp variable to vars_to_report

0 commit comments

Comments
 (0)