Skip to content

Commit 2ac5cc7

Browse files
authored
LFP: electrode_offsets refactor for multi-report support (#3779)
Refactor the LFP electrode metadata representation from a scalar `num_electrodes` to a CSR-style `electrode_offsets` vector, enabling per-report electrode file support for multiple LFP reports with different electrode configurations. Fix: #3784 ## Motivation The SONATA spec is moving `electrodes_file` from the global `run` section to individual LFP report blocks ([sonata-extension#34](BlueBrain/sonata-extension#34), [libsonata katta/per-report-electrodes-file](https://github.com/BlueBrain/libsonata/tree/katta/per-report-electrodes-file)). This enables different LFP reports to use different electrode weight files (e.g. deep probes vs surface EEG). CoreNEURON currently stores a single set of electrode factors per cell with a scalar `num_electrodes`. To support multiple electrode files, we replace this with a CSR-style offsets array and add per-report slicing at report setup time. ## What this PR does 1. Replace `SecMapping::num_electrodes` (int) with `SecMapping::electrode_offsets` (vector<int>) 2. Update `nrnbbcore_register_mapping` to accept a Vector (offsets) instead of scalar int 3. Update the transfer callback to pass `vector<int>` instead of `int` 4. CoreNEURON `CellMapping` stores offsets, `num_electrodes()` derives from `offsets.back()` 5. File format: header line extended with offset count + values 6. `get_lfp_vars_to_report` uses report-specific offset/count slicing for its output buffer 7. neurodamus: passes `Nd.Vector([0, N])` instead of scalar `N` (or `[0, N, N+M]` for multi-report) ## What does NOT change - `lfp_calc()` computation — iterates all electrodes as before (factors are concatenated) - LFP output — bit-for-bit identical for single-report case - Performance — no hot-path allocations added; offsets resolved at setup time only ## Design For a single report with N electrodes: `electrode_offsets = [0, N]` For two reports with 2 and 3 electrodes: `electrode_offsets = [0, 2, 5]` Each LFP report (by order in report.conf) knows its slice: report i uses electrodes `[offsets[i], offsets[i+1])`. The factor vector per segment is the concatenation of all reports' factors. `lfp_calc()` always computes over the full electrode set (all factors). The slicing only matters at report output time in `get_lfp_vars_to_report`. ## Related PRs (separate repos) - **neurodamus:** Will adapt to read `electrodes_file` from per-report blocks (new libsonata API), load multiple electrode files, and pass concatenated offsets. - **libsonata:** [katta/per-report-electrodes-file](https://github.com/BlueBrain/libsonata/tree/katta/per-report-electrodes-file) - **sonata-extension:** [katta/strict-lfp-variable-name](https://github.com/BlueBrain/sonata-extension/tree/katta/strict-lfp-variable-name) ## Testing - All existing ctest LFP tests pass unchanged (behavioral equivalence for single-report) - All neurodamus `test_lfp.py` tests pass unchanged - Multi-report correctness validated against sequential single-report runs
1 parent d272e12 commit 2ac5cc7

16 files changed

Lines changed: 137 additions & 47 deletions

File tree

src/coreneuron/apps/main1.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,11 @@ extern "C" int run_solve_core(int argc, char** argv) {
561561

562562
// register all reports with libsonata
563563
double min_report_dt = INT_MAX;
564+
size_t lfp_report_counter = 0;
564565
for (size_t i = 0; i < configs.size(); i++) {
566+
if (configs[i].type == ReportType::LFP) {
567+
configs[i].lfp_report_index = lfp_report_counter++;
568+
}
565569
std::unique_ptr<ReportHandler> report_handler = create_report_handler(configs[i],
566570
spikes_info);
567571
if (report_handler) {

src/coreneuron/io/nrn2core_direct.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ extern void (*nrn2core_get_dat3_secmapping_)(int i_c,
106106
int& nsec,
107107
int& nseg,
108108
size_t& total_lfp_factors,
109-
int& n_electrodes,
109+
std::vector<size_t>& electrode_offsets,
110110
std::vector<int>& data_sec,
111111
std::vector<int>& data_seg,
112112
std::vector<double>& data_lfp);

src/coreneuron/io/nrn_filehandler.hpp

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include <iostream>
1212
#include <fstream>
13+
#include <sstream>
1314
#include <vector>
1415
#include <cmath>
1516
#include <sys/stat.h>
@@ -118,18 +119,27 @@ class FileHandler {
118119
NrnThreadMappingInfo* ntmapping,
119120
std::shared_ptr<CellMapping> cmap,
120121
const NrnThread& nt) {
121-
int nsec, nseg, n_scan;
122-
size_t total_lfp_factors;
123-
int num_electrodes;
124-
char line_buf[max_line_length], name[max_line_length];
125-
126-
F.getline(line_buf, sizeof(line_buf));
127-
n_scan = sscanf(
128-
line_buf, "%s %d %d %zd %d", name, &nsec, &nseg, &total_lfp_factors, &num_electrodes);
129-
130-
nrn_assert(n_scan == 5);
122+
std::string line;
123+
std::getline(F, line);
124+
125+
std::istringstream iss(line);
126+
std::string name_str;
127+
int nsec = 0;
128+
int nseg = 0;
129+
size_t total_lfp_factors = 0;
130+
int offset_count = 0;
131+
iss >> name_str >> nsec >> nseg >> total_lfp_factors >> offset_count;
132+
nrn_assert(!iss.fail());
133+
134+
if (offset_count > 0) {
135+
cmap->electrode_offsets.resize(offset_count);
136+
for (int k = 0; k < offset_count; k++) {
137+
iss >> cmap->electrode_offsets[k];
138+
}
139+
nrn_assert(!iss.fail());
140+
}
131141

132-
mapinfo->type = section_type_from_string(name);
142+
mapinfo->type = section_type_from_string(name_str);
133143

134144
if (nseg) {
135145
auto sec = read_vector<int>(nseg);
@@ -148,7 +158,7 @@ class FileHandler {
148158
mapinfo->add_segment(sec[i], seg[i]);
149159
ntmapping->add_segment_id(seg[i]);
150160
if (total_lfp_factors > 0) {
151-
// Abort if the factors contains a NaN
161+
const auto num_electrodes = cmap->num_electrodes();
152162
nrn_assert(count_if(lfp_factors.begin(), lfp_factors.end(), [](double d) {
153163
return std::isnan(d);
154164
}) == 0);

src/coreneuron/io/nrnsection_mapping.hpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ struct CellMapping {
8080
/** flat array of lfp factors: stride = num_electrodes, indexed as [i * stride + e] */
8181
std::vector<double> lfp_factors_flat;
8282

83+
/** Electrode offsets per LFP report (CSR-style, size = num_reports + 1).
84+
* offsets[i]..offsets[i+1] gives the electrode range for report i. */
85+
std::vector<size_t> electrode_offsets;
86+
8387
CellMapping(int g)
8488
: gid(g) {}
8589

@@ -103,9 +107,9 @@ struct CellMapping {
103107
});
104108
}
105109

106-
/** @brief return the number of electrodes per segment **/
110+
/** @brief return the total number of electrodes **/
107111
size_t num_electrodes() const {
108-
return lfp_segment_ids.empty() ? 0 : lfp_factors_flat.size() / lfp_segment_ids.size();
112+
return electrode_offsets.empty() ? 0 : electrode_offsets.back();
109113
}
110114

111115
/** @brief number of section lists */

src/coreneuron/io/phase3.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ void (*nrn2core_get_dat3_secmapping_)(int i_c,
2020
int& nsec,
2121
int& nseg,
2222
size_t& total_lfp_factors,
23-
int& n_electrodes,
23+
std::vector<size_t>& electrode_offsets,
2424
std::vector<int>& data_sec,
2525
std::vector<int>& data_seg,
2626
std::vector<double>& data_lfp);
@@ -60,7 +60,6 @@ void Phase3::read_direct(NrnThreadMappingInfo* ntmapping, const NrnThread& nt) {
6060
std::string sclname;
6161
int n_sec;
6262
int n_seg;
63-
int n_electrodes;
6463
size_t total_lfp_factors;
6564
std::vector<int> data_sec;
6665
std::vector<int> data_seg;
@@ -71,10 +70,11 @@ void Phase3::read_direct(NrnThreadMappingInfo* ntmapping, const NrnThread& nt) {
7170
n_sec,
7271
n_seg,
7372
total_lfp_factors,
74-
n_electrodes,
73+
cmap->electrode_offsets,
7574
data_sec,
7675
data_seg,
7776
data_lfp);
77+
const auto n_electrodes = cmap->num_electrodes();
7878
if (nt._permute) {
7979
node_permute(data_seg.data(), data_seg.size(), nt._permute);
8080
}

src/coreneuron/io/reports/nrnreport.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ struct ReportConfiguration {
258258
std::vector<int> point_compartment_ids; // list of compartment_ids for this compartment set
259259
// report (empty otherwise)
260260
Scaling scaling;
261+
size_t lfp_report_index = 0; // position of this LFP report in electrode_offsets
261262
};
262263

263264
void setup_report_engine(double dt_report, double mindelay);

src/coreneuron/io/reports/report_event.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ void ReportEvent::lfp_calc(NrnThread* nt) {
116116
}
117117

118118
// 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];
119+
for (const auto& output: electrode_outputs) {
120+
*(output.var_value) = lfp_values[output.id];
121121
}
122122
}
123123
}

src/coreneuron/io/reports/report_event.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ namespace coreneuron {
2222
struct VarWithMapping {
2323
uint32_t id;
2424
double* var_value;
25-
VarWithMapping(int id_, double* v_)
26-
: id(id_)
25+
VarWithMapping(size_t id_, double* v_)
26+
: id(static_cast<uint32_t>(id_))
2727
, var_value(v_) {}
2828
};
2929

src/coreneuron/io/reports/report_handler.cpp

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,7 @@ static VarsToReport get_synapse_vars_to_report(const NrnThread& nt,
584584
for (auto i = 0; i < var_ptrs.size(); ++i) {
585585
if (selected_vars.empty() || static_cast<bool>(selected_vars[i])) {
586586
vars_to_report[gid].push_back(
587-
{static_cast<int>(*synapseIDs[i]), var_ptrs[i]});
587+
{static_cast<size_t>(*synapseIDs[i]), var_ptrs[i]});
588588
}
589589
}
590590
}
@@ -598,7 +598,8 @@ static VarsToReport get_synapse_vars_to_report(const NrnThread& nt,
598598
static VarsToReport get_lfp_vars_to_report(const NrnThread& nt,
599599
const std::vector<int>& intersection_ids,
600600
ReportConfiguration& report,
601-
double* report_variable) {
601+
double* report_variable,
602+
size_t lfp_report_index) {
602603
const auto* mapinfo = static_cast<NrnThreadMappingInfo*>(nt.mapping);
603604
if (!mapinfo) {
604605
std::cerr << "[ERROR] : LFP report mapping information is missing for a Cell group "
@@ -617,8 +618,15 @@ static VarsToReport get_lfp_vars_to_report(const NrnThread& nt,
617618
nrn_abort(1);
618619
}
619620
std::vector<VarWithMapping> to_report;
620-
int num_electrodes = cell_mapping->num_electrodes();
621-
for (int electrode_id = 0; electrode_id < num_electrodes; electrode_id++) {
621+
const auto& offsets = cell_mapping->electrode_offsets;
622+
size_t start = 0;
623+
size_t end = 0;
624+
if (!offsets.empty()) {
625+
nrn_assert(offsets.size() > lfp_report_index + 1);
626+
start = offsets[lfp_report_index];
627+
end = offsets[lfp_report_index + 1];
628+
}
629+
for (auto electrode_id = start; electrode_id < end; electrode_id++) {
622630
to_report.emplace_back(VarWithMapping(electrode_id, report_variable + offset_lfp));
623631
offset_lfp++;
624632
}
@@ -681,8 +689,11 @@ void ReportHandler::create_report(ReportConfiguration& report_config,
681689
}
682690
case ReportType::LFP: {
683691
mapinfo->prepare_lfp();
684-
vars_to_report =
685-
get_lfp_vars_to_report(nt, intersection_ids, report_config, mapinfo->_lfp.data());
692+
vars_to_report = get_lfp_vars_to_report(nt,
693+
intersection_ids,
694+
report_config,
695+
mapinfo->_lfp.data(),
696+
report_config.lfp_report_index);
686697
register_section_report(nt, report_config, vars_to_report, is_soma_target);
687698
break;
688699
}

src/nrniv/nrncore_write/callbacks/nrncore_callbacks.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ void nrnthread_dat3_secmapping(int i_c,
249249
int& nsec,
250250
int& nseg,
251251
size_t& total_lfp_factors,
252-
int& n_electrodes,
252+
std::vector<size_t>& electrode_offsets,
253253
std::vector<int>& data_sec,
254254
std::vector<int>& data_seg,
255255
std::vector<double>& data_lfp) {
@@ -259,7 +259,9 @@ void nrnthread_dat3_secmapping(int i_c,
259259
nsec = s->nsec;
260260
nseg = s->size();
261261
total_lfp_factors = s->seglfp_factors.size();
262-
n_electrodes = s->num_electrodes;
262+
if (!s->electrode_offsets.empty()) {
263+
electrode_offsets = s->electrode_offsets;
264+
}
263265
data_sec = s->sections;
264266
data_seg = s->segments;
265267
data_lfp = s->seglfp_factors;

0 commit comments

Comments
 (0)