• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

BlueBrain / libsonata / 7029770837

29 Nov 2023 08:15AM UTC coverage: 93.327% (-0.6%) from 93.924%
7029770837

Pull #307

github

1uc
Implement `Hdf5Reader` API and default.

This commit introduces the API for an Hdf5Reader. This reader abstracts the
process of opening HDF5 files, and reading an `libsonata.Selection` from a
dataset.

The default reader calls the existing `_readSelection`.
Pull Request #307: Inject dataset reading via `Hdf5Reader`.

80 of 95 new or added lines in 10 files covered. (84.21%)

7 existing lines in 2 files now uncovered.

1888 of 2023 relevant lines covered (93.33%)

80.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.49
/src/config.cpp
1
/*************************************************************************
2
 * Copyright (C) 2018-2021 Blue Brain Project
3
 *                         Jonas Karlsson <jonas.karlsson@epfl.ch>
4
 *                         Juan Hernando <juan.hernando@epfl.ch>
5
 *
6
 * This file is part of 'libsonata', distributed under the terms
7
 * of the GNU Lesser General Public License version 3.
8
 *
9
 * See top-level COPYING.LESSER and COPYING files for details.
10
 *************************************************************************/
11

12
#include <bbp/sonata/config.h>
13

14
#include <bbp/sonata/optional.hpp>
15
#include <regex>
16
#include <set>
17
#include <string>
18

19
#include <fmt/format.h>
20
#include <nlohmann/json.hpp>
21

22
#include "../extlib/filesystem.hpp"
23
#include "utils.h"
24

25
// Add a specialization of adl_serializer to the nlohmann namespace for conversion from/to
26
// nonstd::optional
27
namespace nlohmann {
28
template <typename T>
29
struct adl_serializer<nonstd::optional<T>> {
30
    static void to_json(json& j, const nonstd::optional<T>& opt) {
31
        if (opt == nonstd::nullopt) {
32
            j = nullptr;
33
        } else {
34
            j = *opt;
35
        }
36
    }
37

38
    static void from_json(const json& j, nonstd::optional<T>& opt) {
50✔
39
        if (j.is_null()) {
50✔
40
            opt = nonstd::nullopt;
6✔
41
        } else {
42
            opt = j.get<T>();
44✔
43
        }
44
    }
50✔
45
};
46
}  // namespace nlohmann
47

48
namespace bbp {
49
namespace sonata {
50

51
NLOHMANN_JSON_SERIALIZE_ENUM(CircuitConfig::ConfigStatus,
×
52
                             {{CircuitConfig::ConfigStatus::invalid, nullptr},
53
                              {CircuitConfig::ConfigStatus::partial, "partial"},
54
                              {CircuitConfig::ConfigStatus::complete, "complete"}})
55

56
NLOHMANN_JSON_SERIALIZE_ENUM(SimulationConfig::Conditions::SpikeLocation,
24✔
57
                             {{SimulationConfig::Conditions::SpikeLocation::invalid, nullptr},
58
                              {SimulationConfig::Conditions::SpikeLocation::soma, "soma"},
59
                              {SimulationConfig::Conditions::SpikeLocation::AIS, "AIS"}})
60

61
NLOHMANN_JSON_SERIALIZE_ENUM(SimulationConfig::Run::IntegrationMethod,
30✔
62
                             {{SimulationConfig::Run::IntegrationMethod::invalid, nullptr},
63
                              {SimulationConfig::Run::IntegrationMethod::euler, 0},
64
                              {SimulationConfig::Run::IntegrationMethod::nicholson, 1},
65
                              {SimulationConfig::Run::IntegrationMethod::nicholson_ion, 2}})
66

67
NLOHMANN_JSON_SERIALIZE_ENUM(SimulationConfig::Output::SpikesSortOrder,
16✔
68
                             {{SimulationConfig::Output::SpikesSortOrder::invalid, nullptr},
69
                              {SimulationConfig::Output::SpikesSortOrder::none, "none"},
70
                              {SimulationConfig::Output::SpikesSortOrder::by_id, "by_id"},
71
                              {SimulationConfig::Output::SpikesSortOrder::by_time, "by_time"}})
72

73
NLOHMANN_JSON_SERIALIZE_ENUM(SimulationConfig::Report::Sections,
68✔
74
                             {{SimulationConfig::Report::Sections::invalid, nullptr},
75
                              {SimulationConfig::Report::Sections::soma, "soma"},
76
                              {SimulationConfig::Report::Sections::axon, "axon"},
77
                              {SimulationConfig::Report::Sections::dend, "dend"},
78
                              {SimulationConfig::Report::Sections::apic, "apic"},
79
                              {SimulationConfig::Report::Sections::all, "all"}})
80
NLOHMANN_JSON_SERIALIZE_ENUM(SimulationConfig::Report::Type,
140✔
81
                             {{SimulationConfig::Report::Type::invalid, nullptr},
82
                              {SimulationConfig::Report::Type::compartment, "compartment"},
83
                              {SimulationConfig::Report::Type::lfp, "lfp"},
84
                              {SimulationConfig::Report::Type::summation, "summation"},
85
                              {SimulationConfig::Report::Type::synapse, "synapse"}})
86
NLOHMANN_JSON_SERIALIZE_ENUM(SimulationConfig::Report::Scaling,
12✔
87
                             {{SimulationConfig::Report::Scaling::invalid, nullptr},
88
                              {SimulationConfig::Report::Scaling::none, "none"},
89
                              {SimulationConfig::Report::Scaling::area, "area"}})
90
NLOHMANN_JSON_SERIALIZE_ENUM(SimulationConfig::Report::Compartments,
12✔
91
                             {{SimulationConfig::Report::Compartments::invalid, nullptr},
92
                              {SimulationConfig::Report::Compartments::center, "center"},
93
                              {SimulationConfig::Report::Compartments::all, "all"}})
94

95
NLOHMANN_JSON_SERIALIZE_ENUM(
690✔
96
    SimulationConfig::InputBase::Module,
97
    {{SimulationConfig::InputBase::Module::invalid, nullptr},
98
     {SimulationConfig::InputBase::Module::linear, "linear"},
99
     {SimulationConfig::InputBase::Module::relative_linear, "relative_linear"},
100
     {SimulationConfig::InputBase::Module::pulse, "pulse"},
101
     {SimulationConfig::InputBase::Module::subthreshold, "subthreshold"},
102
     {SimulationConfig::InputBase::Module::hyperpolarizing, "hyperpolarizing"},
103
     {SimulationConfig::InputBase::Module::synapse_replay, "synapse_replay"},
104
     {SimulationConfig::InputBase::Module::seclamp, "seclamp"},
105
     {SimulationConfig::InputBase::Module::noise, "noise"},
106
     {SimulationConfig::InputBase::Module::shot_noise, "shot_noise"},
107
     {SimulationConfig::InputBase::Module::relative_shot_noise, "relative_shot_noise"},
108
     {SimulationConfig::InputBase::Module::absolute_shot_noise, "absolute_shot_noise"},
109
     {SimulationConfig::InputBase::Module::ornstein_uhlenbeck, "ornstein_uhlenbeck"},
110
     {SimulationConfig::InputBase::Module::relative_ornstein_uhlenbeck,
111
      "relative_ornstein_uhlenbeck"}})
112

113
NLOHMANN_JSON_SERIALIZE_ENUM(
354✔
114
    SimulationConfig::InputBase::InputType,
115
    {{SimulationConfig::InputBase::InputType::invalid, nullptr},
116
     {SimulationConfig::InputBase::InputType::spikes, "spikes"},
117
     {SimulationConfig::InputBase::InputType::extracellular_stimulation,
118
      "extracellular_stimulation"},
119
     {SimulationConfig::InputBase::InputType::current_clamp, "current_clamp"},
120
     {SimulationConfig::InputBase::InputType::voltage_clamp, "voltage_clamp"},
121
     {SimulationConfig::InputBase::InputType::conductance, "conductance"}})
122

123
NLOHMANN_JSON_SERIALIZE_ENUM(SimulationConfig::SimulatorType,
24✔
124
                             {{SimulationConfig::SimulatorType::invalid, nullptr},
125
                              {SimulationConfig::SimulatorType::NEURON, "NEURON"},
126
                              {SimulationConfig::SimulatorType::CORENEURON, "CORENEURON"}})
127

128
NLOHMANN_JSON_SERIALIZE_ENUM(
28✔
129
    SimulationConfig::ModificationBase::ModificationType,
130
    {{SimulationConfig::ModificationBase::ModificationType::invalid, nullptr},
131
     {SimulationConfig::ModificationBase::ModificationType::TTX, "TTX"},
132
     {SimulationConfig::ModificationBase::ModificationType::ConfigureAllSections,
133
      "ConfigureAllSections"}})
134

135
namespace {
136
// to be replaced by std::filesystem once C++17 is used
137
namespace fs = ghc::filesystem;
138

139
void raiseOnBiophysicalPopulationErrors(const std::string& population,
26✔
140
                                        const bbp::sonata::NodePopulationProperties& properties) {
141
    if (properties.morphologiesDir.empty() && properties.alternateMorphologyFormats.empty()) {
26✔
142
        throw SonataError(
143
            fmt::format("Node population '{}' is defined as 'biophysical' "
2✔
144
                        "but does not define 'morphologies_dir' or "
145
                        "'alternateMorphologyFormats'",
146
                        population));
6✔
147
    } else if (properties.biophysicalNeuronModelsDir.empty()) {
24✔
148
        throw SonataError(
149
            fmt::format("Node population '{}' is defined as 'biophysical' "
2✔
150
                        "but does not define 'biophysical_neuron_models_dir'",
151
                        population));
6✔
152
    }
153
}
22✔
154

155
void raiseOnVasculaturePopulationErrors(const std::string& population,
×
156
                                        const bbp::sonata::NodePopulationProperties& properties) {
157
    if (!properties.vasculatureFile.has_value() || properties.vasculatureFile.value().empty()) {
×
158
        throw SonataError(
159
            fmt::format("Node population '{}' is defined as 'vasculature' "
×
160
                        "but does not define 'vasculature_file",
161
                        population));
×
162
    } else if (!properties.vasculatureMesh.has_value() ||
×
163
               properties.vasculatureMesh.value().empty()) {
×
164
        throw SonataError(
165
            fmt::format("Node population '{}' is defined as 'vasculature' "
×
166
                        "but does not define 'vasculature_mesh",
167
                        population));
×
168
    }
169
}
×
170

171
void raiseOnAstrocytePopulationErrors(const std::string& population,
×
172
                                      const bbp::sonata::NodePopulationProperties& properties) {
173
    if (!properties.microdomainsFile.has_value() || properties.microdomainsFile.value().empty()) {
×
174
        throw SonataError(
175
            fmt::format("Node population '{}' is defined as 'astrocyte' "
×
176
                        "but does not define 'microdomains_file",
177
                        population));
×
178
    }
179
}
×
180

181
void raiseOnEndfootPopulationErrors(const std::string& population,
×
182
                                    const bbp::sonata::EdgePopulationProperties& properties) {
183
    if (!properties.endfeetMeshesFile.has_value() || properties.endfeetMeshesFile.value().empty()) {
×
184
        throw SonataError(
185
            fmt::format("Node population '{}' is defined as 'endfoot' "
×
186
                        "but does not define 'endfeet_meshes_file",
187
                        population));
×
188
    }
189
}
×
190

191
template <typename PopulationType>
192
PopulationType getPopulationProperties(
30✔
193
    const std::string& populationName,
194
    const std::unordered_map<std::string, PopulationType>& populations) {
195
    auto it = populations.find(populationName);
30✔
196
    if (it == populations.end()) {
30✔
197
        throw SonataError(fmt::format("Could not find population '{}'", populationName));
16✔
198
    }
199

200
    return it->second;
44✔
201
}
202

203
template <typename PopulationType, typename PopulationPropertiesT>
204
PopulationType getPopulation(const std::string& populationName,
8✔
205
                             const std::unordered_map<std::string, PopulationPropertiesT>& src,
206
                             const Hdf5Reader& hdf5_reader) {
207
    const auto properties = getPopulationProperties(populationName, src);
12✔
208
    return PopulationType(properties.elementsPath,
209
                          properties.typesPath,
210
                          populationName,
211
                          hdf5_reader);
8✔
212
}
213

214
std::map<std::string, std::string> replaceVariables(std::map<std::string, std::string> variables) {
104✔
215
    constexpr size_t maxIterations = 10;
104✔
216

217
    bool anyChange = true;
104✔
218
    size_t iteration = 0;
104✔
219

220
    while (anyChange) {
248✔
221
        anyChange = false;
148✔
222
        auto variablesCopy = variables;
296✔
223

224
        for (const auto& vI : variables) {
286✔
225
            const auto& vIKey = vI.first;
138✔
226
            const auto& vIValue = vI.second;
138✔
227

228
            for (auto& vJ : variablesCopy) {
432✔
229
                auto& vJValue = vJ.second;
294✔
230
                const auto startPos = vJValue.find(vIKey);
294✔
231

232
                if (startPos != std::string::npos) {
294✔
233
                    vJValue.replace(startPos, vIKey.length(), vIValue);
70✔
234
                    anyChange = true;
70✔
235
                }
236
            }
237
        }
238
        variables = variablesCopy;
148✔
239

240
        if (++iteration == maxIterations) {
148✔
241
            throw SonataError(
242
                "Reached maximum allowed iterations in variable expansion, "
243
                "possibly infinite recursion.");
4✔
244
        }
245
    }
246

247
    return variables;
100✔
248
}
249

250
nlohmann::json expandVariables(const nlohmann::json& json,
100✔
251
                               const std::map<std::string, std::string>& vars) {
252
    auto jsonFlat = json.flatten();
200✔
253

254
    // Expand variables in whole json
255
    for (auto it = jsonFlat.begin(); it != jsonFlat.end(); ++it) {
1,654✔
256
        auto& value = it.value();
1,554✔
257
        if (!value.is_string()) {
1,554✔
258
            continue;
826✔
259
        }
260

261
        auto valueStr = value.get<std::string>();
1,456✔
262

263
        for (const auto& var : vars) {
1,964✔
264
            const auto& varName = var.first;
1,236✔
265
            const auto& varValue = var.second;
1,236✔
266
            const auto startPos = valueStr.find(varName);
1,236✔
267

268
            if (startPos != std::string::npos) {
1,236✔
269
                valueStr.replace(startPos, varName.length(), varValue);
122✔
270
                value = fs::path(valueStr).lexically_normal();
122✔
271
            }
272
        }
273
    }
274

275
    return jsonFlat.unflatten();
200✔
276
}
277

278
using Variables = std::map<std::string, std::string>;
279

280
Variables readVariables(const nlohmann::json& json) {
106✔
281
    Variables variables;
106✔
282

283
    if (json.find("manifest") == json.end()) {
106✔
284
        return variables;
66✔
285
    }
286

287
    const auto& manifest = json["manifest"];
40✔
288

289
    const std::regex regexVariable(R"(\$[a-zA-Z0-9_]*)");
80✔
290

291
    for (auto it = manifest.begin(); it != manifest.end(); ++it) {
100✔
292
        const auto& name = it.key();
62✔
293

294
        if (std::regex_match(name, regexVariable)) {
62✔
295
            variables[name] = it.value();
60✔
296
        } else {
297
            throw SonataError(fmt::format("Invalid variable `{}`", name));
4✔
298
        }
299
    }
300

301
    return variables;
38✔
302
}
303

304
std::string toAbsolute(const fs::path& base, const fs::path& path) {
186✔
305
    const auto absolute = path.is_absolute() ? path : fs::absolute(base / path);
186✔
306
    return absolute.lexically_normal().string();
558✔
307
}
308

309
template <typename Type, std::enable_if_t<std::is_enum<Type>::value>* = nullptr>
310
void raiseIfInvalidEnum(const char* name,
234✔
311
                        const Type& buf,
312
                        const std::string& found_value,
313
                        std::true_type /* tag */) {
314
    if (buf == Type::invalid) {
234✔
315
        throw SonataError(fmt::format("Invalid value: '{}' for key '{}'", found_value, name));
32✔
316
    }
317
}
218✔
318

319
template <typename Type>
320
void raiseIfInvalidEnum(const char* /*unused*/,
862✔
321
                        const Type& /*unused*/,
322
                        const std::string& /*unused*/,
323
                        std::false_type /* tag */) {}
862✔
324

325
template <typename Type>
326
void parseMandatory(const nlohmann::json& it,
880✔
327
                    const char* name,
328
                    const std::string& section_name,
329
                    Type& buf) {
330
    const auto element = it.find(name);
880✔
331
    if (element == it.end()) {
880✔
332
        throw SonataError(fmt::format("Could not find '{}' in '{}'", name, section_name));
60✔
333
    }
334
    buf = element->get<Type>();
850✔
335

336
    raiseIfInvalidEnum(name, buf, element->dump(), std::is_enum<Type>());
860✔
337
}
840✔
338

339
template <typename Type>
340
void parseOptional(const nlohmann::json& it,
808✔
341
                   const char* name,
342
                   Type& buf,
343
                   nonstd::optional<Type> default_value = nonstd::nullopt) {
344
    const auto element = it.find(name);
808✔
345
    if (element != it.end()) {
808✔
346
        buf = element->get<Type>();
246✔
347
        raiseIfInvalidEnum(name, buf, element->dump(), std::is_enum<Type>());
252✔
348
    } else if (default_value != nonstd::nullopt) {
562✔
349
        buf = default_value.value();
512✔
350
    }
351
}
802✔
352

353
void parseVariantType(const nlohmann::json& it, variantValueType& var) {
48✔
354
    switch (it.type()) {
48✔
355
    case nlohmann::json::value_t::boolean:
12✔
356
        var = it.get<bool>();
12✔
357
        break;
12✔
358
    case nlohmann::json::value_t::string:
12✔
359
        var = it.get<std::string>();
12✔
360
        break;
12✔
361
    case nlohmann::json::value_t::number_float:
12✔
362
        var = it.get<double>();
12✔
363
        break;
12✔
364
    case nlohmann::json::value_t::number_integer:
12✔
365
    case nlohmann::json::value_t::number_unsigned:
366
        var = it.get<int>();
12✔
367
        break;
12✔
368
    default:
×
369
        throw SonataError("Value type not supported");
×
370
    }
371
}
48✔
372

373
SimulationConfig::Input parseInputModule(const nlohmann::json& valueIt,
68✔
374
                                         const SimulationConfig::InputBase::Module module,
375
                                         const std::string& basePath,
376
                                         const std::string& debugStr) {
377
    using Module = SimulationConfig::InputBase::Module;
378

379
    const auto parseCommon = [&](auto& input) {
68✔
380
        input.module = module;
68✔
381
        parseMandatory(valueIt, "input_type", debugStr, input.inputType);
68✔
382
        parseMandatory(valueIt, "delay", debugStr, input.delay);
66✔
383
        parseMandatory(valueIt, "duration", debugStr, input.duration);
66✔
384
        parseMandatory(valueIt, "node_set", debugStr, input.nodeSet);
66✔
385
    };
134✔
386

387
    switch (module) {
68✔
388
    case Module::linear: {
6✔
389
        SimulationConfig::InputLinear ret;
12✔
390
        parseCommon(ret);
6✔
391
        parseMandatory(valueIt, "amp_start", debugStr, ret.ampStart);
4✔
392
        parseOptional(valueIt, "amp_end", ret.ampEnd, {ret.ampStart});
4✔
393
        return ret;
4✔
394
    }
395
    case Module::relative_linear: {
4✔
396
        SimulationConfig::InputRelativeLinear ret;
8✔
397
        parseCommon(ret);
4✔
398
        parseMandatory(valueIt, "percent_start", debugStr, ret.percentStart);
4✔
399
        parseOptional(valueIt, "percent_end", ret.percentEnd, {ret.percentStart});
4✔
400
        return ret;
4✔
401
    }
402
    case Module::pulse: {
4✔
403
        SimulationConfig::InputPulse ret;
8✔
404
        parseCommon(ret);
4✔
405
        parseMandatory(valueIt, "amp_start", debugStr, ret.ampStart);
4✔
406
        parseMandatory(valueIt, "width", debugStr, ret.width);
4✔
407
        parseMandatory(valueIt, "frequency", debugStr, ret.frequency);
4✔
408
        parseOptional(valueIt, "amp_end", ret.ampEnd, {ret.ampStart});
4✔
409
        return ret;
4✔
410
    }
411
    case Module::subthreshold: {
4✔
412
        SimulationConfig::InputSubthreshold ret;
8✔
413
        parseCommon(ret);
4✔
414
        parseMandatory(valueIt, "percent_less", debugStr, ret.percentLess);
4✔
415
        return ret;
4✔
416
    }
417
    case Module::noise: {
12✔
418
        SimulationConfig::InputNoise ret;
24✔
419
        parseCommon(ret);
12✔
420
        const auto mean = valueIt.find("mean");
12✔
421
        const auto mean_percent = valueIt.find("mean_percent");
12✔
422

423
        if (mean != valueIt.end()) {
12✔
424
            parseOptional(valueIt, "mean", ret.mean);
12✔
425
        }
426

427
        if (mean_percent != valueIt.end()) {
12✔
428
            parseOptional(valueIt, "mean_percent", ret.meanPercent);
6✔
429
        }
430

431
        if (ret.mean.has_value() && ret.meanPercent.has_value()) {
12✔
432
            throw SonataError("Both `mean` or `mean_percent` have values in " + debugStr);
2✔
433
        } else if (!ret.mean.has_value() && !ret.meanPercent.has_value()) {
10✔
434
            throw SonataError("One of `mean` or `mean_percent` need to have a value in " +
4✔
435
                              debugStr);
6✔
436
        }
437

438
        parseOptional(valueIt, "variance", ret.variance);
8✔
439
        return ret;
8✔
440
    }
441
    case Module::shot_noise: {
4✔
442
        SimulationConfig::InputShotNoise ret;
8✔
443
        parseCommon(ret);
4✔
444
        parseMandatory(valueIt, "rise_time", debugStr, ret.riseTime);
4✔
445
        parseMandatory(valueIt, "decay_time", debugStr, ret.decayTime);
4✔
446
        parseOptional(valueIt, "random_seed", ret.randomSeed);
4✔
447
        parseOptional(valueIt, "reversal", ret.reversal, {0.0});
4✔
448
        parseOptional(valueIt, "dt", ret.dt, {0.25});
4✔
449
        parseMandatory(valueIt, "rate", debugStr, ret.rate);
4✔
450
        parseMandatory(valueIt, "amp_mean", debugStr, ret.ampMean);
4✔
451
        parseMandatory(valueIt, "amp_var", debugStr, ret.ampVar);
4✔
452
        return ret;
4✔
453
    }
454
    case Module::relative_shot_noise: {
4✔
455
        SimulationConfig::InputRelativeShotNoise ret;
8✔
456
        parseCommon(ret);
4✔
457

458
        parseMandatory(valueIt, "rise_time", debugStr, ret.riseTime);
4✔
459
        parseMandatory(valueIt, "decay_time", debugStr, ret.decayTime);
4✔
460
        parseOptional(valueIt, "random_seed", ret.randomSeed);
4✔
461
        parseOptional(valueIt, "reversal", ret.reversal, {0.0});
4✔
462
        parseOptional(valueIt, "dt", ret.dt, {0.25});
4✔
463
        parseMandatory(valueIt, "amp_cv", debugStr, ret.ampCv);
4✔
464
        parseMandatory(valueIt, "mean_percent", debugStr, ret.meanPercent);
4✔
465
        parseMandatory(valueIt, "sd_percent", debugStr, ret.sdPercent);
4✔
466
        return ret;
4✔
467
    }
468
    case Module::absolute_shot_noise: {
4✔
469
        SimulationConfig::InputAbsoluteShotNoise ret;
8✔
470
        parseCommon(ret);
4✔
471

472
        parseMandatory(valueIt, "rise_time", debugStr, ret.riseTime);
4✔
473
        parseMandatory(valueIt, "decay_time", debugStr, ret.decayTime);
4✔
474
        parseOptional(valueIt, "random_seed", ret.randomSeed);
4✔
475
        parseOptional(valueIt, "reversal", ret.reversal, {0.0});
4✔
476
        parseOptional(valueIt, "dt", ret.dt, {0.25});
4✔
477
        parseMandatory(valueIt, "amp_cv", debugStr, ret.ampCv);
4✔
478
        parseMandatory(valueIt, "mean", debugStr, ret.mean);
4✔
479
        parseMandatory(valueIt, "sigma", debugStr, ret.sigma);
4✔
480
        return ret;
4✔
481
    }
482
    case Module::hyperpolarizing: {
6✔
483
        SimulationConfig::InputHyperpolarizing ret;
12✔
484
        parseCommon(ret);
6✔
485
        return ret;
6✔
486
    }
487
    case Module::synapse_replay: {
8✔
488
        SimulationConfig::InputSynapseReplay ret;
16✔
489
        parseCommon(ret);
8✔
490
        parseMandatory(valueIt, "spike_file", debugStr, ret.spikeFile);
8✔
491
        parseOptional(valueIt, "source", ret.source);
8✔
492
        ret.spikeFile = toAbsolute(basePath, ret.spikeFile);
8✔
493
        return ret;
8✔
494
    }
495
    case Module::seclamp: {
4✔
496
        SimulationConfig::InputSeclamp ret;
8✔
497
        parseCommon(ret);
4✔
498
        parseMandatory(valueIt, "voltage", debugStr, ret.voltage);
4✔
499
        parseOptional(valueIt, "series_resistance", ret.seriesResistance, {0.01});
4✔
500
        return ret;
4✔
501
    }
502
    case Module::ornstein_uhlenbeck: {
4✔
503
        SimulationConfig::InputOrnsteinUhlenbeck ret;
8✔
504
        parseCommon(ret);
4✔
505
        parseMandatory(valueIt, "tau", debugStr, ret.tau);
4✔
506
        parseOptional(valueIt, "reversal", ret.reversal, {0.0});
4✔
507
        parseOptional(valueIt, "random_seed", ret.randomSeed);
4✔
508
        parseOptional(valueIt, "dt", ret.dt, {0.25});
4✔
509

510
        parseMandatory(valueIt, "mean", debugStr, ret.mean);
4✔
511
        parseMandatory(valueIt, "sigma", debugStr, ret.sigma);
4✔
512
        return ret;
4✔
513
    }
514
    case Module::relative_ornstein_uhlenbeck: {
4✔
515
        SimulationConfig::InputRelativeOrnsteinUhlenbeck ret;
8✔
516
        parseCommon(ret);
4✔
517
        parseMandatory(valueIt, "tau", debugStr, ret.tau);
4✔
518
        parseOptional(valueIt, "reversal", ret.reversal, {0.0});
4✔
519
        parseOptional(valueIt, "random_seed", ret.randomSeed);
4✔
520
        parseOptional(valueIt, "dt", ret.dt, {0.25});
4✔
521

522
        parseMandatory(valueIt, "mean_percent", debugStr, ret.meanPercent);
4✔
523
        parseMandatory(valueIt, "sd_percent", debugStr, ret.sdPercent);
4✔
524
        return ret;
4✔
525
    }
526
    default:
×
527
        throw SonataError("Unknown module for the input_type in " + debugStr);
×
528
    }
529
}
530

531
void parseConditionsMechanisms(
4✔
532
    const nlohmann::json& it,
533
    std::unordered_map<std::string, std::unordered_map<std::string, variantValueType>>& buf) {
534
    const auto mechIt = it.find("mechanisms");
4✔
535
    if (mechIt == it.end()) {
4✔
536
        return;
×
537
    }
538
    for (auto& scopeIt : mechIt->items()) {
12✔
539
        std::unordered_map<std::string, variantValueType> map_vars;
8✔
540
        for (auto& varIt : scopeIt.value().items()) {
24✔
541
            variantValueType res_val;
16✔
542
            parseVariantType(varIt.value(), res_val);
16✔
543
            map_vars.insert({varIt.key(), res_val});
16✔
544
        }
545
        buf.insert({scopeIt.key(), map_vars});
8✔
546
    }
547
}
548

549
void parseConditionsModifications(const nlohmann::json& it,
4✔
550
                                  SimulationConfig::ModificationMap& buf) {
551
    const auto sectionIt = it.find("modifications");
4✔
552
    if (sectionIt == it.end()) {
4✔
553
        return;
×
554
    }
555
    for (auto& mIt : sectionIt->items()) {
12✔
556
        const auto valueIt = mIt.value();
16✔
557
        const auto debugStr = fmt::format("modification {}", mIt.key());
16✔
558

559
        SimulationConfig::ModificationBase::ModificationType type;
560
        parseMandatory(valueIt, "type", debugStr, type);
8✔
561

562
        switch (type) {
8✔
563
        case SimulationConfig::ModificationBase::ModificationType::TTX: {
4✔
564
            SimulationConfig::ModificationTTX result;
8✔
565
            result.type = type;
4✔
566
            parseMandatory(valueIt, "node_set", debugStr, result.nodeSet);
4✔
567
            buf[mIt.key()] = result;
4✔
568
            break;
4✔
569
        }
570
        case SimulationConfig::ModificationBase::ModificationType::ConfigureAllSections: {
4✔
571
            SimulationConfig::ModificationConfigureAllSections result;
8✔
572
            result.type = type;
4✔
573
            parseMandatory(valueIt, "node_set", debugStr, result.nodeSet);
4✔
574
            parseMandatory(valueIt, "section_configure", debugStr, result.sectionConfigure);
4✔
575
            buf[mIt.key()] = result;
4✔
576
            break;
4✔
577
        }
578
        default:
×
579
            throw SonataError("Unknown modificationn type in " + debugStr);
×
580
        }
581
    }
582
}
583

584
}  // namespace
585

586
class CircuitConfig::Parser
587
{
588
  public:
589
    Parser(const std::string& contents, const std::string& basePath)
34✔
590
        : _basePath(fs::absolute(fs::path(basePath))) {
46✔
591
        // Parse and expand JSON string
592
        const auto rawJson = nlohmann::json::parse(contents);
68✔
593
        const auto vars = replaceVariables(readVariables(rawJson));
38✔
594
        _json = expandVariables(rawJson, vars);
28✔
595
    }
28✔
596

597
    template <typename T>
598
    T getJSONValue(const nlohmann::json& json,
518✔
599
                   const std::string& key,
600
                   const T& defaultValue = T()) const {
601
        auto it = json.find(key);
518✔
602
        if (it != json.end() && !it->is_null()) {
518✔
603
            return it.value().get<T>();
110✔
604
        }
605

606
        return defaultValue;
408✔
607
    }
608

609
    nonstd::optional<std::string> getOptionalJSONPath(const nlohmann::json& json,
228✔
610
                                                      const std::string& key) const {
611
        auto value = getJSONValue<std::string>(json, key);
456✔
612
        if (!value.empty()) {
228✔
613
            return toAbsolute(_basePath, value);
16✔
614
        }
615

616
        return nonstd::nullopt;
212✔
617
    }
618

619
    std::string getJSONPath(const nlohmann::json& json,
242✔
620
                            const std::string& key,
621
                            const std::string& defaultValue = std::string()) const {
622
        auto value = getJSONValue<std::string>(json, key);
484✔
623
        if (!value.empty()) {
242✔
624
            return toAbsolute(_basePath, value);
82✔
625
        }
626

627
        return defaultValue;
160✔
628
    }
629

630
    ConfigStatus getCircuitConfigStatus() const {
28✔
631
        if (_json.find("metadata") == _json.end()) {
28✔
632
            return ConfigStatus::complete;
28✔
633
        }
634
        const auto& metadata = _json.at("metadata");
×
635
        const auto res = getJSONValue<ConfigStatus>(metadata, "status", {ConfigStatus::complete});
×
636

637
        if (res == ConfigStatus::invalid) {
×
638
            throw SonataError("Invalid value for `metadata::ConfigStatus` in config");
×
639
        }
640

641
        return res;
×
642
    }
643

644
    nlohmann::json getSubNetworkJson(const std::string& prefix,
50✔
645
                                     CircuitConfig::ConfigStatus ConfigStatus) const {
646
        // Fail if no network entry is defined
647
        if (_json.find("networks") == _json.end()) {
50✔
648
            if (ConfigStatus == CircuitConfig::ConfigStatus::complete) {
2✔
649
                throw SonataError("Error parsing config: `networks` not specified");
2✔
650
            } else {
651
                return {};
×
652
            }
653
        }
654

655
        const auto& networks = _json.at("networks");
48✔
656

657
        const std::string component = prefix + "s";
96✔
658
        if (networks.find(component) == networks.end()) {
48✔
659
            if (ConfigStatus == CircuitConfig::ConfigStatus::complete) {
6✔
660
                throw SonataError(
661
                    fmt::format("Error parsing networks config: '{}' not specified", component));
12✔
662
            } else {
663
                return {};
×
664
            }
665
        }
666

667
        return networks.at(component);
42✔
668
    }
669

670
    std::string getNodeSetsPath() const {
28✔
671
        // Retrieve node sets file, if any
672
        if (_json.find("node_sets_file") != _json.end()) {
28✔
673
            return toAbsolute(_basePath, _json["node_sets_file"]);
8✔
674
        }
675

676
        return {};
20✔
677
    }
678

679
    CircuitConfig::Components parseDefaultComponents() const {
18✔
680
        CircuitConfig::Components result;
18✔
681

682
        if (_json.find("components") == _json.end()) {
18✔
683
            return result;
×
684
        }
685

686
        const auto& components = _json.at("components");
18✔
687

688
        result.morphologiesDir = getJSONPath(components, "morphologies_dir");
18✔
689

690
        const auto alternateMorphoDir = components.find("alternate_morphologies");
18✔
691
        if (alternateMorphoDir != components.end()) {
18✔
692
            for (auto it = alternateMorphoDir->begin(); it != alternateMorphoDir->end(); ++it) {
12✔
693
                result.alternateMorphologiesDir[it.key()] = toAbsolute(_basePath, it.value());
6✔
694
            }
695
        }
696

697
        result.biophysicalNeuronModelsDir = getJSONPath(components,
36✔
698
                                                        "biophysical_neuron_models_dir");
18✔
699

700
        result.vasculatureFile = getOptionalJSONPath(components, "vasculature_file");
18✔
701
        result.vasculatureMesh = getOptionalJSONPath(components, "vasculature_mesh");
18✔
702
        result.endfeetMeshesFile = getOptionalJSONPath(components, "endfeet_meshes_file");
18✔
703
        result.microdomainsFile = getOptionalJSONPath(components, "microdomains_file");
18✔
704
        result.spineMorphologiesDir = getOptionalJSONPath(components, "spine_morphologies_dir");
18✔
705

706
        return result;
18✔
707
    }
708

709
    template <typename PopulationPropertiesT>
710
    void updateDefaultProperties(PopulationPropertiesT& component,
48✔
711
                                 const std::string& defaultType,
712
                                 const CircuitConfig::Components& defaultComponents) {
713
        if (component.type.empty()) {
48✔
714
            component.type = defaultType;
40✔
715
        }
716

717
        if (component.alternateMorphologyFormats.empty()) {
48✔
718
            component.alternateMorphologyFormats = defaultComponents.alternateMorphologiesDir;
44✔
719
        }
720

721
        if (component.biophysicalNeuronModelsDir.empty()) {
48✔
722
            component.biophysicalNeuronModelsDir = defaultComponents.biophysicalNeuronModelsDir;
46✔
723
        }
724

725
        if (component.morphologiesDir.empty()) {
48✔
726
            component.morphologiesDir = defaultComponents.morphologiesDir;
44✔
727
        }
728
    }
48✔
729

730
    void updateDefaultNodeProperties(std::unordered_map<std::string, NodePopulationProperties>& map,
18✔
731
                                     const std::string& defaultType,
732
                                     const CircuitConfig::Components& defaultComponents) {
733
        for (auto& entry : map) {
48✔
734
            auto& component = entry.second;
30✔
735
            updateDefaultProperties(component, defaultType, defaultComponents);
30✔
736

737
            if (!component.vasculatureFile) {
30✔
738
                component.vasculatureFile = defaultComponents.vasculatureFile;
30✔
739
            }
740

741
            if (!component.vasculatureMesh) {
30✔
742
                component.vasculatureMesh = defaultComponents.vasculatureMesh;
30✔
743
            }
744

745
            if (!component.microdomainsFile) {
30✔
746
                component.microdomainsFile = defaultComponents.microdomainsFile;
30✔
747
            }
748
            if (!component.spineMorphologiesDir) {
30✔
749
                component.spineMorphologiesDir = defaultComponents.spineMorphologiesDir;
30✔
750
            }
751
        }
752
    }
18✔
753

754
    void updateDefaultEdgeProperties(std::unordered_map<std::string, EdgePopulationProperties>& map,
18✔
755
                                     const std::string& defaultType,
756
                                     const CircuitConfig::Components& defaultComponents) {
757
        for (auto& entry : map) {
36✔
758
            auto& component = entry.second;
18✔
759
            updateDefaultProperties(component, defaultType, defaultComponents);
18✔
760

761
            if (!component.endfeetMeshesFile) {
18✔
762
                component.endfeetMeshesFile = defaultComponents.endfeetMeshesFile;
18✔
763
            }
764
        }
765
    }
18✔
766

767
    template <typename JSON>
768
    std::tuple<std::string, std::string> parseSubNetworks(const std::string& prefix,
32✔
769
                                                          const JSON& value) const {
770
        const std::string elementsFile = prefix + "s_file";
64✔
771
        auto h5File = getJSONPath(value, elementsFile);
64✔
772

773
        if (h5File.empty()) {
32✔
774
            throw SonataError(
775
                fmt::format("'{}' network do not define '{}' entry", prefix, elementsFile));
4✔
776
        }
777

778
        const std::string typesFile = prefix + "_types_file";
60✔
779
        auto csvFile = getJSONPath(value, typesFile);
60✔
780

781
        return {h5File, csvFile};
60✔
782
    }
783

784
    template <typename PopulationProperties, typename Func>
785
    std::unordered_map<std::string, PopulationProperties> parsePopulationProperties(
50✔
786
        const std::string& prefix, CircuitConfig::ConfigStatus status, const Func& parser) const {
787
        const auto& network = getSubNetworkJson(prefix, status);
92✔
788

789
        std::unordered_map<std::string, PopulationProperties> output;
42✔
790

791
        for (const auto& subnetwork : network) {
102✔
792
            std::string elementsPath;
34✔
793
            std::string typesPath;
34✔
794
            std::tie(elementsPath, typesPath) = parseSubNetworks(prefix, subnetwork);
32✔
795

796
            const auto populationsIt = subnetwork.find("populations");
30✔
797
            if (populationsIt == subnetwork.end()) {
30✔
798
                continue;
4✔
799
            }
800

801
            for (auto it = populationsIt->begin(); it != populationsIt->end(); ++it) {
74✔
802
                const auto& popData = it.value();
48✔
803

804
                if (output.find(it.key()) != output.end()) {
48✔
805
                    throw SonataError(fmt::format("Population {} is declared twice", it.key()));
×
806
                }
807

808
                PopulationProperties& popProperties = output[it.key()];
48✔
809

810
                popProperties.elementsPath = elementsPath;
48✔
811
                popProperties.typesPath = typesPath;
48✔
812

813
                popProperties.type = getJSONValue<std::string>(popData, "type");
48✔
814
                popProperties.morphologiesDir = getJSONPath(popData, "morphologies_dir");
48✔
815
                popProperties.biophysicalNeuronModelsDir =
48✔
816
                    getJSONPath(popData, "biophysical_neuron_models_dir");
817

818
                // Overwrite those specified, if any
819
                const auto altMorphoDir = popData.find("alternate_morphologies");
48✔
820
                if (altMorphoDir != popData.end()) {
48✔
821
                    for (auto it = altMorphoDir->begin(); it != altMorphoDir->end(); ++it) {
8✔
822
                        popProperties.alternateMorphologyFormats[it.key()] = toAbsolute(_basePath,
4✔
823
                                                                                        it.value());
4✔
824
                    }
825
                }
826

827
                parser(popProperties, popData);
48✔
828
            }
829
        }
830

831
        return output;
80✔
832
    }
833

834
    std::unordered_map<std::string, NodePopulationProperties> parseNodePopulations(
28✔
835
        CircuitConfig::ConfigStatus status) const {
836
        return parsePopulationProperties<NodePopulationProperties>(
837
            "node",
838
            status,
839
            [&](NodePopulationProperties& popProperties, const nlohmann::json& popData) {
30✔
840
            popProperties.spatialSegmentIndexDir = getJSONPath(popData, "spatial_segment_index_dir");
30✔
841
            popProperties.vasculatureFile = getOptionalJSONPath(popData, "vasculature_file");
30✔
842
            popProperties.vasculatureMesh = getOptionalJSONPath(popData, "vasculature_mesh");
30✔
843
            popProperties.microdomainsFile = getOptionalJSONPath(popData, "microdomains_file");
30✔
844
            popProperties.spineMorphologiesDir = getOptionalJSONPath(popData,
60✔
845
                                                                     "spine_morphologies_dir");
30✔
846
            });
70✔
847
    }
848

849
    std::unordered_map<std::string, EdgePopulationProperties> parseEdgePopulations(
22✔
850
        CircuitConfig::ConfigStatus status) const {
851
        return parsePopulationProperties<EdgePopulationProperties>(
852
            "edge",
853
            status,
854
            [&](EdgePopulationProperties& popProperties, const nlohmann::json& popData) {
18✔
855
            popProperties.spatialSynapseIndexDir = getJSONPath(popData, "spatial_synapse_index_dir");
18✔
856
            popProperties.endfeetMeshesFile = getOptionalJSONPath(popData, "endfeet_meshes_file");
18✔
857
            });
48✔
858
    }
859

860
    std::string getExpandedJSON() const {
28✔
861
        return _json.dump();
28✔
862
    }
863

864
  private:
865
    const fs::path _basePath;
866
    nlohmann::json _json;
867
};
868

869
CircuitConfig::CircuitConfig(const std::string& contents, const std::string& basePath) {
114✔
870
    Parser parser(contents, basePath);
62✔
871

872
    _expandedJSON = parser.getExpandedJSON();
28✔
873
    _status = parser.getCircuitConfigStatus();
28✔
874

875
    _nodeSetsFile = parser.getNodeSetsPath();
28✔
876

877
    _nodePopulationProperties = parser.parseNodePopulations(_status);
28✔
878
    _edgePopulationProperties = parser.parseEdgePopulations(_status);
22✔
879

880
    Components defaultComponents = parser.parseDefaultComponents();
36✔
881

882
    parser.updateDefaultNodeProperties(_nodePopulationProperties, "biophysical", defaultComponents);
18✔
883
    parser.updateDefaultEdgeProperties(_edgePopulationProperties, "chemical", defaultComponents);
18✔
884

885
    if (getCircuitConfigStatus() == ConfigStatus::complete) {
18✔
886
        for (const auto& it : _nodePopulationProperties) {
44✔
887
            const auto& population = it.first;
30✔
888
            const auto& properties = it.second;
30✔
889
            if (properties.type == "biophysical") {
30✔
890
                raiseOnBiophysicalPopulationErrors(population, properties);
26✔
891
            } else if (properties.type == "vasculature") {
4✔
892
                raiseOnVasculaturePopulationErrors(population, properties);
×
893
            } else if (properties.type == "astrocyte") {
4✔
894
                raiseOnAstrocytePopulationErrors(population, properties);
×
895
            }
896
        }
897

898
        for (const auto& it : _edgePopulationProperties) {
32✔
899
            const auto& population = it.first;
18✔
900
            const auto& properties = it.second;
18✔
901
            if (properties.type == "endfoot") {
18✔
902
                raiseOnEndfootPopulationErrors(population, properties);
×
903
            }
904
        }
905
    }
906
}
14✔
907

908
CircuitConfig CircuitConfig::fromFile(const std::string& path) {
14✔
909
    return {readFile(path), fs::path(path).parent_path()};
14✔
910
}
911

912
CircuitConfig::ConfigStatus CircuitConfig::getCircuitConfigStatus() const {
18✔
913
    return _status;
18✔
914
}
915

916
const std::string& CircuitConfig::getNodeSetsPath() const {
10✔
917
    return _nodeSetsFile;
10✔
918
}
919

920
std::set<std::string> CircuitConfig::listNodePopulations() const {
2✔
921
    return getMapKeys(_nodePopulationProperties);
2✔
922
}
923

924
NodePopulation CircuitConfig::getNodePopulation(const std::string& name) const {
4✔
925
    return getNodePopulation(name, Hdf5Reader());
6✔
926
}
927

928
NodePopulation CircuitConfig::getNodePopulation(const std::string& name,
4✔
929
                                                const Hdf5Reader& hdf5_reader) const {
930
    return getPopulation<NodePopulation>(name, _nodePopulationProperties, hdf5_reader);
4✔
931
}
932

933
std::set<std::string> CircuitConfig::listEdgePopulations() const {
2✔
934
    return getMapKeys(_edgePopulationProperties);
2✔
935
}
936

937
EdgePopulation CircuitConfig::getEdgePopulation(const std::string& name) const {
4✔
938
    return getPopulation<EdgePopulation>(name, _edgePopulationProperties, Hdf5Reader());
6✔
939
}
940

NEW
941
EdgePopulation CircuitConfig::getEdgePopulation(const std::string& name,
×
942
                                                const Hdf5Reader& hdf5_reader) const {
NEW
943
    return getPopulation<EdgePopulation>(name, _edgePopulationProperties, hdf5_reader);
×
944
}
945

946
NodePopulationProperties CircuitConfig::getNodePopulationProperties(const std::string& name) const {
12✔
947
    return getPopulationProperties(name, _nodePopulationProperties);
12✔
948
}
949

950
EdgePopulationProperties CircuitConfig::getEdgePopulationProperties(const std::string& name) const {
10✔
951
    return getPopulationProperties(name, _edgePopulationProperties);
10✔
952
}
953

954
const std::string& CircuitConfig::getExpandedJSON() const {
4✔
955
    return _expandedJSON;
4✔
956
}
957

958
class SimulationConfig::Parser
959
{
960
  public:
961
    Parser(const std::string& content, const std::string& basePath)
72✔
962
        : _basePath(fs::absolute(fs::path(basePath)).lexically_normal()) {
72✔
963
        // Parse manifest section and expand JSON string
964
        const auto rawJson = nlohmann::json::parse(content);
144✔
965
        const auto vars = replaceVariables(readVariables(rawJson));
72✔
966
        _json = expandVariables(rawJson, vars);
72✔
967
    }
72✔
968

969
    SimulationConfig::Run parseRun() const {
72✔
970
        const auto runIt = _json.find("run");
72✔
971
        if (runIt == _json.end()) {
72✔
972
            throw SonataError("Could not find 'run' section");
2✔
973
        }
974

975
        SimulationConfig::Run result{};
70✔
976
        parseMandatory(*runIt, "tstop", "run", result.tstop);
74✔
977
        parseMandatory(*runIt, "dt", "run", result.dt);
72✔
978
        parseMandatory(*runIt, "random_seed", "run", result.randomSeed);
98✔
979
        parseOptional(*runIt, "spike_threshold", result.spikeThreshold, {-30});
50✔
980
        parseOptional(*runIt,
52✔
981
                      "integration_method",
982
                      result.integrationMethod,
50✔
983
                      {Run::IntegrationMethod::euler});
52✔
984
        parseOptional(*runIt, "stimulus_seed", result.stimulusSeed, {0});
48✔
985
        parseOptional(*runIt, "ionchannel_seed", result.ionchannelSeed, {0});
48✔
986
        parseOptional(*runIt, "minis_seed", result.minisSeed, {0});
48✔
987
        parseOptional(*runIt, "synapse_seed", result.synapseSeed, {0});
48✔
988
        parseOptional(*runIt, "electrodes_file", result.electrodesFile, {""});
48✔
989

990
        if (!result.electrodesFile.empty()) {
48✔
991
            result.electrodesFile = toAbsolute(_basePath, result.electrodesFile);
4✔
992
        }
993

994
        return result;
96✔
995
    }
996

997
    SimulationConfig::Output parseOutput() const {
48✔
998
        SimulationConfig::Output result{};
48✔
999

1000
        const auto outputIt = _json.find("output");
48✔
1001
        if (outputIt == _json.end()) {
48✔
1002
            return result;
44✔
1003
        }
1004
        parseOptional(*outputIt, "output_dir", result.outputDir, {"output"});
4✔
1005
        parseOptional(*outputIt, "log_file", result.logFile, {""});
4✔
1006
        parseOptional(*outputIt, "spikes_file", result.spikesFile, {"out.h5"});
4✔
1007
        parseOptional(*outputIt,
4✔
1008
                      "spikes_sort_order",
1009
                      result.sortOrder,
4✔
1010
                      {Output::SpikesSortOrder::by_time});
4✔
1011

1012
        result.outputDir = toAbsolute(_basePath, result.outputDir);
4✔
1013

1014
        return result;
4✔
1015
    }
1016

1017
    SimulationConfig::Conditions parseConditions() const {
48✔
1018
        SimulationConfig::Conditions result{};
48✔
1019

1020
        const auto conditionsIt = _json.find("conditions");
48✔
1021
        if (conditionsIt == _json.end()) {
48✔
1022
            return result;
42✔
1023
        }
1024
        parseOptional(*conditionsIt, "celsius", result.celsius, {34.0});
6✔
1025
        parseOptional(*conditionsIt, "v_init", result.vInit, {-80});
6✔
1026
        parseOptional(*conditionsIt,
8✔
1027
                      "spike_location",
1028
                      result.spikeLocation,
6✔
1029
                      {Conditions::SpikeLocation::soma});
8✔
1030
        parseOptional(*conditionsIt, "extracellular_calcium", result.extracellularCalcium);
4✔
1031
        parseOptional(*conditionsIt,
8✔
1032
                      "randomize_gaba_rise_time",
1033
                      result.randomizeGabaRiseTime,
4✔
1034
                      {false});
1035
        parseConditionsMechanisms(*conditionsIt, result.mechanisms);
4✔
1036
        parseConditionsModifications(*conditionsIt, result.modifications);
4✔
1037
        return result;
4✔
1038
    }
1039

1040
    ReportMap parseReports(const SimulationConfig::Output& output) const {
46✔
1041
        ReportMap result;
46✔
1042

1043
        const auto reportsIt = _json.find("reports");
46✔
1044
        if (reportsIt == _json.end()) {
46✔
1045
            return result;
22✔
1046
        }
1047

1048
        for (auto it = reportsIt->begin(); it != reportsIt->end(); ++it) {
44✔
1049
            auto& report = result[it.key()];
40✔
1050
            const auto& valueIt = it.value();
40✔
1051
            const std::string debugStr = "report " + it.key();
80✔
1052

1053
            parseOptional(valueIt, "cells", report.cells, parseNodeSet());
40✔
1054
            parseOptional(valueIt, "sections", report.sections, {Report::Sections::soma});
40✔
1055
            parseMandatory(valueIt, "type", debugStr, report.type);
40✔
1056
            parseOptional(valueIt, "scaling", report.scaling, {Report::Scaling::area});
36✔
1057
            parseOptional(valueIt,
36✔
1058
                          "compartments",
1059
                          report.compartments,
36✔
1060
                          {report.sections == Report::Sections::soma ? Report::Compartments::center
36✔
1061
                                                                     : Report::Compartments::all});
1062
            parseMandatory(valueIt, "variable_name", debugStr, report.variableName);
36✔
1063
            parseOptional(valueIt, "unit", report.unit, {"mV"});
34✔
1064
            parseMandatory(valueIt, "dt", debugStr, report.dt);
34✔
1065
            parseMandatory(valueIt, "start_time", debugStr, report.startTime);
32✔
1066
            parseMandatory(valueIt, "end_time", debugStr, report.endTime);
30✔
1067
            parseOptional(valueIt, "file_name", report.fileName, {it.key() + ".h5"});
28✔
1068
            parseOptional(valueIt, "enabled", report.enabled, {true});
28✔
1069

1070
            // variable names can look like:
1071
            // `v`, or `i_clamp`, or `Foo.bar` but not `..asdf`, or `asdf..` or `asdf.asdf.asdf`
1072
            const char* const varName = R"(\w+(?:\.?\w+)?)";
28✔
1073
            // variable names are separated by `,` with any amount of whitespace separating them
1074
            const std::regex expr(fmt::format(R"({}(?:\s*,\s*{})*)", varName, varName));
56✔
1075
            if (!std::regex_match(report.variableName, expr)) {
28✔
1076
                throw SonataError(fmt::format("Invalid comma separated variable names '{}'",
8✔
1077
                                              report.variableName));
24✔
1078
            }
1079

1080
            const auto extension = fs::path(report.fileName).extension().string();
40✔
1081
            if (extension.empty() || extension != ".h5") {
20✔
1082
                report.fileName += ".h5";
8✔
1083
            }
1084
            report.fileName = toAbsolute(output.outputDir, report.fileName);
20✔
1085
        }
1086

1087
        return result;
4✔
1088
    }
1089

1090
    std::string parseNetwork() const {
34✔
1091
        auto val = _json.value("network", "circuit_config.json");
68✔
1092
        return toAbsolute(_basePath, val);
68✔
1093
    }
1094

1095
    SimulationConfig::SimulatorType parseTargetSimulator() const {
10✔
1096
        SimulationConfig::SimulatorType val;
1097
        parseOptional(_json, "target_simulator", val, {SimulationConfig::SimulatorType::NEURON});
12✔
1098
        return val;
8✔
1099
    }
1100

1101
    std::string parseNodeSetsFile() const noexcept {
8✔
1102
        std::string val;
16✔
1103
        if (_json.contains("node_sets_file")) {
8✔
1104
            val = _json["node_sets_file"];
×
1105
            return toAbsolute(_basePath, val);
×
1106
        } else {
1107
            try {
1108
                const auto circuitFile = parseNetwork();
16✔
1109
                const auto conf = CircuitConfig::fromFile(circuitFile);
12✔
1110
                return conf.getNodeSetsPath();
4✔
1111
            } catch (...) {
4✔
1112
                // Don't throw CircuitConfig exceptions in SimulationConfig and return empty string
1113
                return val;
4✔
1114
            }
1115
        }
1116
    }
1117

1118
    nonstd::optional<std::string> parseNodeSet() const {
48✔
1119
        if (_json.contains("node_set")) {
48✔
1120
            return {_json["node_set"]};
24✔
1121
        } else {
1122
            return nonstd::nullopt;
24✔
1123
        }
1124
    }
1125

1126
    InputMap parseInputs() const {
26✔
1127
        InputMap result;
26✔
1128

1129
        const auto inputsIt = _json.find("inputs");
26✔
1130
        if (inputsIt == _json.end()) {
26✔
1131
            return result;
8✔
1132
        }
1133

1134
        for (auto it = inputsIt->begin(); it != inputsIt->end(); ++it) {
78✔
1135
            const auto& valueIt = it.value();
74✔
1136
            const auto debugStr = fmt::format("input {}", it.key());
148✔
1137

1138
            InputBase::Module module;
1139
            parseMandatory(valueIt, "module", debugStr, module);
74✔
1140

1141
            const auto input = parseInputModule(valueIt, module, _basePath, debugStr);
136✔
1142
            result[it.key()] = input;
62✔
1143

1144
            auto mismatchingModuleInputType = [&it]() {
4✔
1145
                const auto module_name = it->find("module")->get<std::string>();
4✔
1146
                const auto input_type = it->find("input_type")->get<std::string>();
4✔
1147
                throw SonataError(
1148
                    fmt::format("An `input` has module `{}` and input_type `{}` which mismatch",
2✔
1149
                                module_name,
1150
                                input_type));
6✔
1151
            };
62✔
1152

1153
            auto inputType = nonstd::visit([](const auto& v) { return v.inputType; }, input);
124✔
1154
            switch (inputType) {
62✔
1155
            case InputBase::InputType::current_clamp: {
40✔
1156
                if (!(nonstd::holds_alternative<SimulationConfig::InputLinear>(input) ||
76✔
1157
                      nonstd::holds_alternative<SimulationConfig::InputRelativeLinear>(input) ||
36✔
1158
                      nonstd::holds_alternative<SimulationConfig::InputPulse>(input) ||
32✔
1159
                      nonstd::holds_alternative<SimulationConfig::InputSubthreshold>(input) ||
28✔
1160
                      nonstd::holds_alternative<SimulationConfig::InputNoise>(input) ||
24✔
1161
                      nonstd::holds_alternative<SimulationConfig::InputShotNoise>(input) ||
16✔
1162
                      nonstd::holds_alternative<SimulationConfig::InputRelativeShotNoise>(input) ||
12✔
1163
                      nonstd::holds_alternative<SimulationConfig::InputAbsoluteShotNoise>(input) ||
8✔
1164
                      nonstd::holds_alternative<SimulationConfig::InputHyperpolarizing>(input) ||
8✔
1165
                      nonstd::holds_alternative<SimulationConfig::InputOrnsteinUhlenbeck>(input) ||
4✔
1166
                      nonstd::holds_alternative<SimulationConfig::InputRelativeOrnsteinUhlenbeck>(
4✔
1167
                          input))) {
1168
                    mismatchingModuleInputType();
×
1169
                }
1170
            } break;
40✔
1171
            case InputBase::InputType::spikes:
4✔
1172
                if (!nonstd::holds_alternative<SimulationConfig::InputSynapseReplay>(input)) {
4✔
1173
                    mismatchingModuleInputType();
×
1174
                }
1175
                break;
4✔
1176
            case InputBase::InputType::voltage_clamp:
6✔
1177
                if (!nonstd::holds_alternative<SimulationConfig::InputSeclamp>(input)) {
6✔
1178
                    mismatchingModuleInputType();
2✔
1179
                }
1180
                break;
4✔
1181
            case InputBase::InputType::extracellular_stimulation:
4✔
1182
                break;
4✔
1183
            case InputBase::InputType::conductance:
8✔
1184
                if (!(nonstd::holds_alternative<SimulationConfig::InputShotNoise>(input) ||
16✔
1185
                      nonstd::holds_alternative<SimulationConfig::InputRelativeShotNoise>(input) ||
8✔
1186
                      nonstd::holds_alternative<SimulationConfig::InputAbsoluteShotNoise>(input) ||
8✔
1187
                      nonstd::holds_alternative<SimulationConfig::InputOrnsteinUhlenbeck>(input) ||
4✔
1188
                      nonstd::holds_alternative<SimulationConfig::InputRelativeOrnsteinUhlenbeck>(
×
1189
                          input))) {
1190
                    mismatchingModuleInputType();
×
1191
                }
1192
                break;
8✔
1193
            default:
×
1194
                throw SonataError(fmt::format("Unknown input_type in {}", debugStr));
×
1195
            }
1196
        }
1197
        return result;
4✔
1198
    }
1199

1200
    std::vector<ConnectionOverride> parseConnectionOverrides() const {
12✔
1201
        std::vector<ConnectionOverride> result;
12✔
1202

1203
        const auto connIt = _json.find("connection_overrides");
12✔
1204
        // nlohmann::json::flatten().unflatten() converts empty containers to `null`:
1205
        // https://json.nlohmann.me/api/basic_json/unflatten/#notes
1206
        // so we can't tell the difference between {} and []; however, since these are
1207
        // empty, we will assume the intent was to have no connection_overrides and forgo
1208
        // better error reporting
1209
        if (connIt == _json.end() || connIt->is_null()) {
12✔
1210
            return result;
6✔
1211
        }
1212

1213
        if (!connIt->is_array()) {
6✔
1214
            throw SonataError("`connection_overrides` must be an array");
2✔
1215
        }
1216

1217
        result.reserve(connIt->size());
4✔
1218

1219
        for (auto it = connIt->begin(); it != connIt->end(); ++it) {
12✔
1220
            const auto& valueIt = it.value();
8✔
1221
            ConnectionOverride connect;
16✔
1222
            parseMandatory(valueIt, "name", "connection_override", connect.name);
8✔
1223
            const auto debugStr = fmt::format("connection_override {}", connect.name);
8✔
1224
            parseMandatory(valueIt, "source", debugStr, connect.source);
8✔
1225
            parseMandatory(valueIt, "target", debugStr, connect.target);
8✔
1226
            parseOptional(valueIt, "weight", connect.weight);
8✔
1227
            parseOptional(valueIt, "spont_minis", connect.spontMinis);
8✔
1228
            parseOptional(valueIt, "synapse_configure", connect.synapseConfigure);
8✔
1229
            parseOptional(valueIt, "modoverride", connect.modoverride);
8✔
1230
            parseOptional(valueIt, "synapse_delay_override", connect.synapseDelayOverride);
8✔
1231
            parseOptional(valueIt, "delay", connect.delay);
8✔
1232
            parseOptional(valueIt, "neuromodulation_dtc", connect.neuromodulationDtc);
8✔
1233
            parseOptional(valueIt, "neuromodulation_strength", connect.neuromodulationStrength);
8✔
1234

1235
            result.push_back(std::move(connect));
8✔
1236
        }
1237
        return result;
4✔
1238
    }
1239

1240
    std::unordered_map<std::string, variantValueType> parseMetaData() const {
8✔
1241
        std::unordered_map<std::string, variantValueType> result;
8✔
1242
        const auto metaIt = _json.find("metadata");
8✔
1243
        if (metaIt == _json.end()) {
8✔
1244
            return result;
4✔
1245
        }
1246
        for (auto& it : metaIt->items()) {
20✔
1247
            variantValueType res_val;
16✔
1248
            parseVariantType(it.value(), res_val);
16✔
1249
            result.insert({it.key(), res_val});
16✔
1250
        }
1251
        return result;
4✔
1252
    }
1253

1254
    std::unordered_map<std::string, variantValueType> parseBetaFeatures() const {
8✔
1255
        std::unordered_map<std::string, variantValueType> result;
8✔
1256
        const auto fIt = _json.find("beta_features");
8✔
1257
        if (fIt == _json.end()) {
8✔
1258
            return result;
4✔
1259
        }
1260
        for (auto& it : fIt->items()) {
20✔
1261
            variantValueType res_val;
16✔
1262
            parseVariantType(it.value(), res_val);
16✔
1263
            result.insert({it.key(), res_val});
16✔
1264
        }
1265
        return result;
4✔
1266
    }
1267

1268
    std::string getExpandedJSON() const {
72✔
1269
        return _json.dump();
72✔
1270
    }
1271

1272
  private:
1273
    const fs::path _basePath;
1274
    nlohmann::json _json;
1275
};
1276

1277
SimulationConfig::SimulationConfig(const std::string& content, const std::string& basePath)
72✔
1278
    : _basePath(fs::absolute(basePath).lexically_normal().string()) {
904✔
1279
    const Parser parser(content, basePath);
136✔
1280
    _expandedJSON = parser.getExpandedJSON();
72✔
1281
    _run = parser.parseRun();
72✔
1282
    _output = parser.parseOutput();
48✔
1283
    _conditions = parser.parseConditions();
48✔
1284
    _reports = parser.parseReports(_output);
46✔
1285
    _network = parser.parseNetwork();
26✔
1286
    _inputs = parser.parseInputs();
26✔
1287
    _connection_overrides = parser.parseConnectionOverrides();
12✔
1288
    _targetSimulator = parser.parseTargetSimulator();
10✔
1289
    _nodeSetsFile = parser.parseNodeSetsFile();
8✔
1290
    _nodeSet = parser.parseNodeSet();
8✔
1291
    _metaData = parser.parseMetaData();
8✔
1292
    _betaFeatures = parser.parseBetaFeatures();
8✔
1293
}
8✔
1294

1295
SimulationConfig SimulationConfig::fromFile(const std::string& path) {
4✔
1296
    return SimulationConfig(readFile(path), fs::path(path).parent_path());
8✔
1297
}
1298

1299
const std::string& SimulationConfig::getBasePath() const noexcept {
2✔
1300
    return _basePath;
2✔
1301
}
1302

1303
const SimulationConfig::Run& SimulationConfig::getRun() const noexcept {
32✔
1304
    return _run;
32✔
1305
}
1306

1307
const SimulationConfig::Output& SimulationConfig::getOutput() const noexcept {
14✔
1308
    return _output;
14✔
1309
}
1310

1311
const SimulationConfig::Conditions& SimulationConfig::getConditions() const noexcept {
28✔
1312
    return _conditions;
28✔
1313
}
1314

1315
const std::string& SimulationConfig::getNetwork() const noexcept {
6✔
1316
    return _network;
6✔
1317
}
1318

1319
std::set<std::string> SimulationConfig::listReportNames() const {
2✔
1320
    return getMapKeys(_reports);
2✔
1321
}
1322

1323
const SimulationConfig::Report& SimulationConfig::getReport(const std::string& name) const {
34✔
1324
    const auto it = _reports.find(name);
34✔
1325
    if (it == _reports.end()) {
34✔
1326
        throw SonataError(
1327
            fmt::format("The report '{}' is not present in the simulation config file", name));
4✔
1328
    }
1329

1330
    return it->second;
32✔
1331
}
1332

1333
std::set<std::string> SimulationConfig::listInputNames() const {
2✔
1334
    return getMapKeys(_inputs);
2✔
1335
}
1336

1337
const SimulationConfig::Input& SimulationConfig::getInput(const std::string& name) const {
30✔
1338
    const auto it = _inputs.find(name);
30✔
1339
    if (it == _inputs.end()) {
30✔
1340
        throw SonataError(
1341
            fmt::format("The input '{}' is not present in the simulation config file", name));
4✔
1342
    }
1343
    return it->second;
28✔
1344
}
1345

1346
const std::vector<SimulationConfig::ConnectionOverride>& SimulationConfig::getConnectionOverrides()
2✔
1347
    const noexcept {
1348
    return _connection_overrides;
2✔
1349
}
1350

1351
const SimulationConfig::SimulatorType& SimulationConfig::getTargetSimulator() const {
4✔
1352
    return _targetSimulator;
4✔
1353
}
1354

1355
const std::string& SimulationConfig::getNodeSetsFile() const noexcept {
4✔
1356
    return _nodeSetsFile;
4✔
1357
}
1358

1359
const nonstd::optional<std::string>& SimulationConfig::getNodeSet() const noexcept {
4✔
1360
    return _nodeSet;
4✔
1361
}
1362

1363
const std::unordered_map<std::string, variantValueType>& SimulationConfig::getMetaData() const
2✔
1364
    noexcept {
1365
    return _metaData;
2✔
1366
}
1367

1368
const std::unordered_map<std::string, variantValueType>& SimulationConfig::getBetaFeatures() const
2✔
1369
    noexcept {
1370
    return _betaFeatures;
2✔
1371
}
1372

1373
const std::string& SimulationConfig::getExpandedJSON() const {
2✔
1374
    return _expandedJSON;
2✔
1375
}
1376

1377
std::set<std::string> SimulationConfig::Conditions::listModificationNames() const {
2✔
1378
    return getMapKeys(modifications);
2✔
1379
}
1380

1381
const SimulationConfig::Modification& SimulationConfig::Conditions::getModification(
6✔
1382
    const std::string& name) const {
1383
    const auto it = modifications.find(name);
6✔
1384
    if (it == modifications.end()) {
6✔
1385
        throw SonataError(
1386
            fmt::format("The modification '{}' is not present in the simulation config file",
2✔
1387
                        name));
6✔
1388
    }
1389
    return it->second;
4✔
1390
}
1391

1392
}  // namespace sonata
1393
}  // namespace bbp
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc