Repository: BlueBrain/libsonata (language: python)

Issue:
behavioural difference btw. missing condition block and condition block with a missing entry
Hi, with @edasubert we noticed the following.

1) When there is no conditions block in the simulation_config libsonata returns `0.0` and `0.0` for `celsius` and `v_init`.
```python
<libsonata._libsonata.Conditions object at 0x7ff5d8590c70>
(Pdb) sim_no_cond.impl.conditions.__dir__()
['__init__', '__doc__', '__module__', 'celsius', 'v_init', 'spike_location', 'extracellular_calcium', 'randomize_gaba_rise_time', 'mechanisms', 'list_modification_names', 'modification', 'SpikeLocation', '__new__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']
(Pdb) sim_no_cond.impl.conditions.celsius
0.0
(Pdb) sim_no_cond.impl.conditions.v_init
0.0
```

2) When there is a conditions block but no v_init and no celsius in it then it returns `-80.0` and `34.0` correspondingly.
```python
(Pdb) condition_object
<libsonata._libsonata.Conditions object at 0x7ff5d85960f0>
(Pdb) dir(condition_object)
['SpikeLocation', '__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'celsius', 'extracellular_calcium', 'list_modification_names', 'mechanisms', 'modification', 'randomize_gaba_rise_time', 'spike_location', 'v_init']
(Pdb) condition_object.v_init
-80.0
(Pdb) condition_object.celsius
34.0
```



Discussion / hints:
Could you provide the .json files for reproduction? Thanks.
Sure, here they are:

`simulation_config.json` -> conditions block with missing entries
`simulation_config_no_condition_block` -> missing conditions block

[simulation_config.json](https://github.com/BlueBrain/libsonata/files/13672235/simulation_config.json)
[simulation_config_no_condition_block.json](https://github.com/BlueBrain/libsonata/files/13672236/simulation_config_no_condition_block.json)


Fix patch:
diff --git a/include/bbp/sonata/config.h b/include/bbp/sonata/config.h
index 3809e475..175d64cf 100644
--- a/include/bbp/sonata/config.h
+++ b/include/bbp/sonata/config.h
@@ -11,7 +11,6 @@
 
 #pragma once
 
-#include <memory>  // std::unique_ptr
 #include <set>
 #include <string>
 #include <unordered_map>
@@ -266,6 +265,13 @@ class SONATA_API SimulationConfig
     struct Run {
         enum class IntegrationMethod { invalid = -1, euler, nicholson, nicholson_ion };
 
+        static constexpr int DEFAULT_spikeThreshold = -30;
+        static constexpr IntegrationMethod DEFAULT_IntegrationMethod = IntegrationMethod::euler;
+        static constexpr int DEFAULT_stimulusSeed = 0;
+        static constexpr int DEFAULT_ionchannelSeed = 0;
+        static constexpr int DEFAULT_minisSeed = 0;
+        static constexpr int DEFAULT_synapseSeed = 0;
+
         /// Biological simulation end time in milliseconds
         double tstop{};
         /// Integration step duration in milliseconds
@@ -273,20 +279,20 @@ class SONATA_API SimulationConfig
         /// Random seed
         int randomSeed{};
         /// The spike detection threshold. Default is -30mV
-        int spikeThreshold;
+        int spikeThreshold = DEFAULT_spikeThreshold;
         /// Selects the NEURON/CoreNEURON integration method. This parameter sets the NEURON
         /// global variable h.secondorder. Default 0 ('euler')
-        IntegrationMethod integrationMethod;
+        IntegrationMethod integrationMethod = DEFAULT_IntegrationMethod;
         /// A non-negative integer used for seeding noise stimuli and any other future stochastic
         /// stimuli, default is 0.
-        int stimulusSeed;
+        int stimulusSeed = DEFAULT_stimulusSeed;
         /// A non-negative integer used for seeding stochastic ion channels, default is 0.
-        int ionchannelSeed;
+        int ionchannelSeed = DEFAULT_ionchannelSeed;
         /// A non-negative integer used for seeding the Poisson processes that drives the minis,
         /// default is 0.
-        int minisSeed;
+        int minisSeed = DEFAULT_minisSeed;
         /// A non-negative integer used for seeding stochastic synapses, default is 0.
-        int synapseSeed;
+        int synapseSeed = DEFAULT_synapseSeed;
         /// Filename that contains the weights for the LFP calculation.
         std::string electrodesFile;
     };
@@ -296,14 +302,19 @@ class SONATA_API SimulationConfig
     struct Output {
         enum class SpikesSortOrder { invalid = -1, none, by_id, by_time };
 
+        static constexpr char DEFAULT_outputDir[] = "output";
+        static constexpr char DEFAULT_logFile[] = "";
+        static constexpr char DEFAULT_spikesFile[] = "out.h5";
+        static constexpr SpikesSortOrder DEFAULT_sortOrder = SpikesSortOrder::by_time;
+
         /// Spike report file output directory. Default is "output"
-        std::string outputDir;
+        std::string outputDir = DEFAULT_outputDir;
         /// Filename where console output is written. Default is STDOUT.
-        std::string logFile;
+        std::string logFile = DEFAULT_logFile;
         /// Spike report file name. Default is "out.h5"
-        std::string spikesFile;
+        std::string spikesFile = DEFAULT_spikesFile;
         /// The sorting order of the spike report. Default is "by_time"
-        SpikesSortOrder sortOrder;
+        SpikesSortOrder sortOrder = DEFAULT_sortOrder;
     };
 
     struct ModificationBase {
@@ -333,19 +344,23 @@ class SONATA_API SimulationConfig
      */
     struct Conditions {
         enum class SpikeLocation { invalid = -1, soma, AIS };
+        static constexpr double DEFAULT_celsius = 34.0;
+        static constexpr double DEFAULT_vInit = -80.0;
+        static constexpr SpikeLocation DEFAULT_spikeLocation = SpikeLocation::soma;
+        static constexpr bool DEFAULT_randomizeGabaRiseTime = false;
 
         /// Temperature of experiment. Default is 34.0
-        double celsius;
+        double celsius = DEFAULT_celsius;
         /// Initial membrane voltage in mV. Default is -80
-        double vInit;
+        double vInit = DEFAULT_vInit;
         /// The spike detection location. Can be either ‘soma’ or 'AIS'. Default is 'soma'
-        SpikeLocation spikeLocation;
+        SpikeLocation spikeLocation = DEFAULT_spikeLocation;
         /// Extracellular calcium concentration, being applied to the synapse uHill parameter in
         /// order to scale the U parameter of synapses. Default is None.
         nonstd::optional<double> extracellularCalcium{nonstd::nullopt};
         /// Enable legacy behavior to randomize the GABA_A rise time in the helper functions.
         /// Default is false
-        bool randomizeGabaRiseTime;
+        bool randomizeGabaRiseTime = DEFAULT_randomizeGabaRiseTime;
         /// Properties to assign values to variables in synapse MOD files.
         /// The format is a dictionary with keys being the SUFFIX names and values being
         /// dictionaries of variables' names and values.
diff --git a/python/generated/docstrings.h b/python/generated/docstrings.h
index 310d08b3..08a385ef 100644
--- a/python/generated/docstrings.h
+++ b/python/generated/docstrings.h
@@ -55,8 +55,8 @@ static const char *__doc_bbp_sonata_CircuitConfig_Components_vasculatureMesh = R
 static const char *__doc_bbp_sonata_CircuitConfig_ConfigStatus = R"doc()doc";
 
 static const char *__doc_bbp_sonata_CircuitConfig_ConfigStatus_complete =
-R"doc(all mandatory properties exist, and the the config should return
-correct values in all possible cases)doc";
+R"doc(all mandatory properties exist, and the config should return correct
+values in all possible cases)doc";
 
 static const char *__doc_bbp_sonata_CircuitConfig_ConfigStatus_invalid = R"doc(needed for parsing json contents that are null / not an enum value)doc";
 
@@ -95,6 +95,8 @@ given population, and the edge network it belongs to.
     SonataError if the given population does not exist in any edge
     network.)doc";
 
+static const char *__doc_bbp_sonata_CircuitConfig_getEdgePopulation_2 = R"doc()doc";
+
 static const char *__doc_bbp_sonata_CircuitConfig_getEdgePopulationProperties =
 R"doc(Return a structure containing edge population specific properties,
 falling back to network properties if there are no population-specific
@@ -116,6 +118,8 @@ given population, and the node network it belongs to.
     SonataError if the given population does not exist in any node
     network.)doc";
 
+static const char *__doc_bbp_sonata_CircuitConfig_getNodePopulation_2 = R"doc()doc";
+
 static const char *__doc_bbp_sonata_CircuitConfig_getNodePopulationProperties =
 R"doc(Return a structure containing node population specific properties,
 falling back to network properties if there are no population-specific
@@ -139,7 +143,7 @@ static const char *__doc_bbp_sonata_CircuitConfig_nodePopulationProperties = R"d
 
 static const char *__doc_bbp_sonata_CircuitConfig_nodeSetsFile = R"doc()doc";
 
-static const char *__doc_bbp_sonata_CircuitConfig_status = R"doc(How strict we are checking the circuit config)doc";
+static const char *__doc_bbp_sonata_CircuitConfig_status = R"doc()doc";
 
 static const char *__doc_bbp_sonata_CommonPopulationProperties = R"doc()doc";
 
@@ -179,6 +183,8 @@ static const char *__doc_bbp_sonata_EdgePopulationProperties_spatialSynapseIndex
 
 static const char *__doc_bbp_sonata_EdgePopulation_EdgePopulation = R"doc()doc";
 
+static const char *__doc_bbp_sonata_EdgePopulation_EdgePopulation_2 = R"doc()doc";
+
 static const char *__doc_bbp_sonata_EdgePopulation_afferentEdges = R"doc(Return inbound edges for given node IDs.)doc";
 
 static const char *__doc_bbp_sonata_EdgePopulation_connectingEdges = R"doc(Return edges connecting two given nodes.)doc";
@@ -195,6 +201,97 @@ static const char *__doc_bbp_sonata_EdgePopulation_targetNodeIDs = R"doc(Return
 
 static const char *__doc_bbp_sonata_EdgePopulation_writeIndices = R"doc(Write bidirectional node->edge indices to EdgePopulation HDF5.)doc";
 
+static const char *__doc_bbp_sonata_Hdf5PluginInterface = R"doc()doc";
+
+static const char *__doc_bbp_sonata_Hdf5PluginRead1DInterface = R"doc(Interface for implementing `readSelection<T>(dset, selection)`.)doc";
+
+static const char *__doc_bbp_sonata_Hdf5PluginRead1DInterface_readSelection =
+R"doc(Read the selected subset of the one-dimensional array.
+
+The selection is canonical, i.e. sorted and non-overlapping. The
+dataset is obtained from a `HighFive::File` opened via
+`this->openFile`.)doc";
+
+static const char *__doc_bbp_sonata_Hdf5PluginRead2DInterface = R"doc()doc";
+
+static const char *__doc_bbp_sonata_Hdf5PluginRead2DInterface_readSelection =
+R"doc(Read the Cartesian product of the two selections.
+
+Both selections are canonical, i.e. sorted and non-overlapping. The
+dataset is obtained from a `HighFive::File` opened via
+`this->openFile`.)doc";
+
+static const char *__doc_bbp_sonata_Hdf5Reader =
+R"doc(Abstraction for reading HDF5 datasets.
+
+The Hdf5Reader provides an interface for reading canonical selections
+from datasets. Selections are canonical if they are sorted and don't
+overlap. This allows implementing different optimization strategies,
+such as minimizing bytes read, aggregating nearby reads or using MPI
+collective I/O.
+
+The design uses virtual inheritance, which enables users to inject
+their own reader if needed. This class is the interface used within
+libsonata. It simply delegates to a "plugin", that satisfies the
+interface `Hdf5PluginInterface`.
+
+To enable MPI collective I/O, `libsonata` must call all methods in an
+MPI-collective manner. This implies that the number of times any
+function in `libsonata` calls any of the `Hdf5Reader` methods must not
+depend on the arguments to the function.
+
+Examples:
+
+void wrong(Selection selection) { // Wrong because some MPI ranks
+might return without // calling `readSelection`. if(selection.empty())
+{ return; } hdf5_reader.readSelection(dset, selection); }
+
+void also_wrong(Selection selection) { // Wrong because `hdf5_reader`
+is called `selection.ranges().size()` // number of times. Which could
+be different on each MPI rank. for(auto range : selection.ranges()) {
+hdf5_reader.readSelection(dset, Selection(std::vector{range})); } }
+
+void subtle(Selection selection, bool flag) { // If the flag can
+differ between MPI ranks, this is wrong because // `readSelection` is
+called with different `dset`s. If the `flag` must // be the same on
+all MPI ranks, this is correct. If this happens in // the libsonata
+API, then passing the same `flag` on all MPI ranks becomes // a
+requirement for the users, when using a collective reader. Example: //
+pop.get_attribute(attr_name, selection) if(flag) {
+hdf5_reader.readSelection(dset1, selection); } else {
+hdf5_reader.readSelection(dset2, selection); } }
+
+void correct(Selection selection) { // Correct because no matter which
+branch is taken // `hdf5_reader.readSelection` is called exactly once.
+if(selection.size % 2 == 0) { hdf5_reader.readSelection(dset,
+selection); } else { hdf5_reader.readSelection(dset, {}); } })doc";
+
+static const char *__doc_bbp_sonata_Hdf5Reader_Hdf5Reader = R"doc(Create a valid Hdf5Reader with the default plugin.)doc";
+
+static const char *__doc_bbp_sonata_Hdf5Reader_Hdf5Reader_2 = R"doc(Create an Hdf5Reader with a user supplied plugin.)doc";
+
+static const char *__doc_bbp_sonata_Hdf5Reader_impl = R"doc()doc";
+
+static const char *__doc_bbp_sonata_Hdf5Reader_openFile =
+R"doc(Open the HDF5.
+
+The dataset passed to `readSelection` must be obtained from a file
+open via this method.)doc";
+
+static const char *__doc_bbp_sonata_Hdf5Reader_readSelection =
+R"doc(Read the selected subset of the one-dimensional array.
+
+Both selections are canonical, i.e. sorted and non-overlapping. The
+dataset is obtained from a `HighFive::File` opened via
+`this->openFile`.)doc";
+
+static const char *__doc_bbp_sonata_Hdf5Reader_readSelection_2 =
+R"doc(Read the Cartesian product of the two selections.
+
+Both selections are canonical, i.e. sorted and non-overlapping. The
+dataset is obtained from a `HighFive::File` opened via
+`this->openFile`.)doc";
+
 static const char *__doc_bbp_sonata_NodePopulation = R"doc()doc";
 
 static const char *__doc_bbp_sonata_NodePopulationProperties = R"doc(Node population-specific network information.)doc";
@@ -217,6 +314,8 @@ Only for vasculature node populations where it is mandatory.)doc";
 
 static const char *__doc_bbp_sonata_NodePopulation_NodePopulation = R"doc()doc";
 
+static const char *__doc_bbp_sonata_NodePopulation_NodePopulation_2 = R"doc()doc";
+
 static const char *__doc_bbp_sonata_NodePopulation_matchAttributeValues =
 R"doc(Return selection of where attribute values match value
 
@@ -274,18 +373,18 @@ Parameter ``name``:
     is the name of the node_set rule to be evaluated
 
 Parameter ``population``:
-    is the population overwhich the returned selection will be valid)doc";
+    is the population for which the returned selection will be valid)doc";
 
 static const char *__doc_bbp_sonata_NodeSets_names = R"doc(Names of the node sets available)doc";
 
 static const char *__doc_bbp_sonata_NodeSets_operator_assign = R"doc()doc";
 
-static const char *__doc_bbp_sonata_NodeSets_toJSON = R"doc(Return string version of node sets)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_NodeSets_update =
-R"doc(Update this NodeSets to include the `other` nodeset
+R"doc(Update `this` to include all nodesets from `this` and `other`.
 
-Duplicate names are overriden with the values from `other`
+Duplicate names are overridden with the values from `other`.
 
 The duplicate names are returned.)doc";
 
@@ -301,6 +400,12 @@ static const char *__doc_bbp_sonata_PopulationStorage_PopulationStorage_2 = R"do
 
 static const char *__doc_bbp_sonata_PopulationStorage_PopulationStorage_3 = R"doc()doc";
 
+static const char *__doc_bbp_sonata_PopulationStorage_PopulationStorage_4 = R"doc()doc";
+
+static const char *__doc_bbp_sonata_PopulationStorage_PopulationStorage_5 = R"doc()doc";
+
+static const char *__doc_bbp_sonata_PopulationStorage_PopulationStorage_6 = R"doc()doc";
+
 static const char *__doc_bbp_sonata_PopulationStorage_impl = R"doc()doc";
 
 static const char *__doc_bbp_sonata_PopulationStorage_openPopulation =
@@ -575,8 +680,6 @@ static const char *__doc_bbp_sonata_Selection = R"doc()doc";
 
 static const char *__doc_bbp_sonata_Selection_Selection = R"doc()doc";
 
-static const char *__doc_bbp_sonata_Selection_Selection_2 = R"doc()doc";
-
 static const char *__doc_bbp_sonata_Selection_empty = R"doc()doc";
 
 static const char *__doc_bbp_sonata_Selection_flatSize = R"doc(Total number of elements constituting Selection)doc";
@@ -989,7 +1092,9 @@ R"doc(For compartment type, select compartments to report. Default value:
 
 static const char *__doc_bbp_sonata_SimulationConfig_Report_dt = R"doc(Interval between reporting steps in milliseconds)doc";
 
-static const char *__doc_bbp_sonata_SimulationConfig_Report_enabled = R"doc(Allows for supressing a report so that is not created. Default is true)doc";
+static const char *__doc_bbp_sonata_SimulationConfig_Report_enabled =
+R"doc(Allows for suppressing a report so that is not created. Default is
+true)doc";
 
 static const char *__doc_bbp_sonata_SimulationConfig_Report_endTime = R"doc(Time to stop reporting in milliseconds)doc";
 
diff --git a/src/config.cpp b/src/config.cpp
index 01a84ca8..cb3ece92 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -132,6 +132,25 @@ NLOHMANN_JSON_SERIALIZE_ENUM(
      {SimulationConfig::ModificationBase::ModificationType::ConfigureAllSections,
       "ConfigureAllSections"}})
 
+// { in C++14; one has to declare static constexpr members; this can go away in c++17
+#define D(name) decltype(SimulationConfig::name) constexpr SimulationConfig::name;
+D(Output::DEFAULT_outputDir)
+D(Conditions::DEFAULT_randomizeGabaRiseTime)
+D(Output::DEFAULT_logFile)
+D(Run::DEFAULT_stimulusSeed)
+D(Conditions::DEFAULT_spikeLocation)
+D(Run::DEFAULT_ionchannelSeed)
+D(Output::DEFAULT_sortOrder)
+D(Run::DEFAULT_IntegrationMethod)
+D(Conditions::DEFAULT_vInit)
+D(Run::DEFAULT_minisSeed)
+D(Output::DEFAULT_spikesFile)
+D(Conditions::DEFAULT_celsius)
+D(Run::DEFAULT_synapseSeed)
+D(Run::DEFAULT_spikeThreshold)
+#undef D
+// }
+
 namespace {
 // to be replaced by std::filesystem once C++17 is used
 namespace fs = ghc::filesystem;
@@ -976,15 +995,21 @@ class SimulationConfig::Parser
         parseMandatory(*runIt, "tstop", "run", result.tstop);
         parseMandatory(*runIt, "dt", "run", result.dt);
         parseMandatory(*runIt, "random_seed", "run", result.randomSeed);
-        parseOptional(*runIt, "spike_threshold", result.spikeThreshold, {-30});
+        parseOptional(*runIt,
+                      "spike_threshold",
+                      result.spikeThreshold,
+                      {Run::DEFAULT_spikeThreshold});
         parseOptional(*runIt,
                       "integration_method",
                       result.integrationMethod,
-                      {Run::IntegrationMethod::euler});
-        parseOptional(*runIt, "stimulus_seed", result.stimulusSeed, {0});
-        parseOptional(*runIt, "ionchannel_seed", result.ionchannelSeed, {0});
-        parseOptional(*runIt, "minis_seed", result.minisSeed, {0});
-        parseOptional(*runIt, "synapse_seed", result.synapseSeed, {0});
+                      {Run::DEFAULT_IntegrationMethod});
+        parseOptional(*runIt, "stimulus_seed", result.stimulusSeed, {Run::DEFAULT_stimulusSeed});
+        parseOptional(*runIt,
+                      "ionchannel_seed",
+                      result.ionchannelSeed,
+                      {Run::DEFAULT_ionchannelSeed});
+        parseOptional(*runIt, "minis_seed", result.minisSeed, {Run::DEFAULT_minisSeed});
+        parseOptional(*runIt, "synapse_seed", result.synapseSeed, {Run::DEFAULT_synapseSeed});
         parseOptional(*runIt, "electrodes_file", result.electrodesFile, {""});
 
         if (!result.electrodesFile.empty()) {
@@ -1001,13 +1026,13 @@ class SimulationConfig::Parser
         if (outputIt == _json.end()) {
             return result;
         }
-        parseOptional(*outputIt, "output_dir", result.outputDir, {"output"});
-        parseOptional(*outputIt, "log_file", result.logFile, {""});
-        parseOptional(*outputIt, "spikes_file", result.spikesFile, {"out.h5"});
+        parseOptional(*outputIt, "output_dir", result.outputDir, {Output::DEFAULT_outputDir});
+        parseOptional(*outputIt, "log_file", result.logFile, {Output::DEFAULT_logFile});
+        parseOptional(*outputIt, "spikes_file", result.spikesFile, {Output::DEFAULT_spikesFile});
         parseOptional(*outputIt,
                       "spikes_sort_order",
                       result.sortOrder,
-                      {Output::SpikesSortOrder::by_time});
+                      {Output::DEFAULT_sortOrder});
 
         result.outputDir = toAbsolute(_basePath, result.outputDir);
 
@@ -1015,23 +1040,23 @@ class SimulationConfig::Parser
     }
 
     SimulationConfig::Conditions parseConditions() const {
-        SimulationConfig::Conditions result{};
+        SimulationConfig::Conditions result;
 
         const auto conditionsIt = _json.find("conditions");
         if (conditionsIt == _json.end()) {
             return result;
         }
-        parseOptional(*conditionsIt, "celsius", result.celsius, {34.0});
-        parseOptional(*conditionsIt, "v_init", result.vInit, {-80});
+        parseOptional(*conditionsIt, "celsius", result.celsius, {Conditions::DEFAULT_celsius});
+        parseOptional(*conditionsIt, "v_init", result.vInit, {Conditions::DEFAULT_vInit});
         parseOptional(*conditionsIt,
                       "spike_location",
                       result.spikeLocation,
-                      {Conditions::SpikeLocation::soma});
+                      {Conditions::DEFAULT_spikeLocation});
         parseOptional(*conditionsIt, "extracellular_calcium", result.extracellularCalcium);
         parseOptional(*conditionsIt,
                       "randomize_gaba_rise_time",
                       result.randomizeGabaRiseTime,
-                      {false});
+                      {Conditions::DEFAULT_randomizeGabaRiseTime});
         parseConditionsMechanisms(*conditionsIt, result.mechanisms);
         parseConditionsModifications(*conditionsIt, result.modifications);
         return result;
@@ -1093,7 +1118,7 @@ class SimulationConfig::Parser
     }
 
     SimulationConfig::SimulatorType parseTargetSimulator() const {
-        SimulationConfig::SimulatorType val;
+        SimulationConfig::SimulatorType val = SimulationConfig::SimulatorType::NEURON;
         parseOptional(_json, "target_simulator", val, {SimulationConfig::SimulatorType::NEURON});
         return val;
     }
@@ -1135,7 +1160,7 @@ class SimulationConfig::Parser
             const auto& valueIt = it.value();
             const auto debugStr = fmt::format("input {}", it.key());
 
-            InputBase::Module module;
+            InputBase::Module module = InputBase::Module::invalid;
             parseMandatory(valueIt, "module", debugStr, module);
 
             const auto input = parseInputModule(valueIt, module, _basePath, debugStr);


Test patch:
diff --git a/python/tests/test_config.py b/python/tests/test_config.py
index 84980add..bf379831 100644
--- a/python/tests/test_config.py
+++ b/python/tests/test_config.py
@@ -625,6 +625,19 @@ def test_empty_connection_overrides(self):
         conf = SimulationConfig(contents, "./")
         self.assertEqual(conf.connection_overrides(), [])
 
+    def test_empty_conditions(self):
+        contents = """
+        {
+          "manifest": { "$CIRCUIT_DIR": "./circuit" },
+          "network": "$CIRCUIT_DIR/circuit_config.json",
+          "run": { "random_seed": 12345, "dt": 0.05, "tstop": 1000 },
+          "connection_overrides": []
+        }
+        """
+        conf = SimulationConfig(contents, "./")
+        self.assertEqual(conf.conditions.celsius, 34.0)
+        self.assertEqual(conf.conditions.v_init, -80)
+
     def test_run(self):
         contents = """
         {