From da3dccd035e02894e31c304e2214cb7ff626e526 Mon Sep 17 00:00:00 2001
From: David Hendriks <davidhendriks93@gmail.com>
Date: Sun, 30 Aug 2020 20:47:14 +0100
Subject: [PATCH] making the tests for persistent data actually useful. putting
 the ensemble functions in the gridls

---
 binarycpython/utils/functions.py |  68 +++++++--
 binarycpython/utils/grid.py      | 235 +++++++++++++++++++------------
 tests/test_persistent_data.py    | 129 +++++++++++------
 3 files changed, 287 insertions(+), 145 deletions(-)

diff --git a/binarycpython/utils/functions.py b/binarycpython/utils/functions.py
index 35d293cac..715dbc0b5 100644
--- a/binarycpython/utils/functions.py
+++ b/binarycpython/utils/functions.py
@@ -8,6 +8,9 @@ useful functions for the user
 import json
 import os
 import tempfile
+import copy
+import inspect
+
 from collections import defaultdict
 
 import h5py
@@ -19,15 +22,25 @@ import binary_c_python_api
 # utility functions
 ########################################################
 
-def remove_file(file, verbose=0):
+def verbose_print(message, verbosity, minimal_verbosity):
+    """
+    Function that decides whether to print a message based on the current verbosity
+    and its minimum verbosity
+
+    if verbosity is equal or higher than the minimum, then we print
+    """
+
+    if verbosity >= minimal_verbosity:
+        print(message)
+
+def remove_file(file, verbosity=0):
     """
     Function to remove files but with verbosity
     """
 
     if os.path.exists(file):
         try:
-            if verbose > 0:
-                print("Removed {}".format(file))
+            verbose_print("Removed {}".format(file), verbosity, 1)
             os.remove(file)
 
         except FileNotFoundError as inst:
@@ -116,6 +129,18 @@ def create_hdf5(data_dir, name):
 # version_info functions
 ########################################################
 
+def return_binary_c_version_info(parsed=False):
+    """
+    Function that returns the version information of binary_c
+    """
+
+    version_info = binary_c_python_api.return_version_info().strip()
+
+    if parsed:
+        version_info = parse_binary_c_version_info(version_info)
+
+    return version_info
+
 def parse_binary_c_version_info(version_info_string):
     """
     Function that parses the binary_c version info. Length function with a lot of branches
@@ -666,7 +691,7 @@ def load_logfile(logfile):
 # Ensemble dict functions
 ########################################################
 
-def inspect_dict(dict_1, indent=0):
+def inspect_dict(dict_1, indent=0, print_structure=True):
     """
     Function to inspect a dict.
 
@@ -675,10 +700,16 @@ def inspect_dict(dict_1, indent=0):
     Prints out keys and their value types
     """
 
+    structure_dict = {}
+
     for key, value in dict_1.items():
-        print("\t"*indent, key, type(value))
+        structure_dict[key] = type(value)
+        if print_structure:
+            print("\t"*indent, key, type(value))
         if isinstance(value, dict):
-            inspect_dict(value, indent=indent+1)
+            structure_dict[key] = inspect_dict(value, indent=indent+1)
+    return structure_dict
+
 
 def merge_dicts(dict_1, dict_2):
     """
@@ -715,24 +746,27 @@ def merge_dicts(dict_1, dict_2):
         if isinstance(dict_1[key], (float, int)):
             new_dict[key] = dict_1[key]
         else:
-            new_dict[key] = dict_1[key].deepcopy()
+            copy_dict = copy.deepcopy(dict_1[key])
+            new_dict[key] = copy_dict
 
     for key in unique_to_dict_2:
         if isinstance(dict_2[key], (float, int)):
             new_dict[key] = dict_2[key]
         else:
-            new_dict[key] = dict_2[key].deepcopy()
+            copy_dict = copy.deepcopy(dict_2[key])
+            new_dict[key] = copy_dict
 
     # Go over the common keys:
     for key in overlapping_keys:
-        # See whether the types are actually similar
+        # See whether the types are actually the same
         if not type(dict_1[key]) is type(dict_2[key]):
             print("Error {} and {} are not of the same type and cannot be merged".format(
                 dict_1[key], dict_2[key]))
             raise ValueError
 
-        # TODO: Create a matrix of combinations here.
-        # TODO: Could maybe be more compact
+        # Here we check for the cases that we want to explicitly catch. Ints will be added,
+        # floats will be added, lists will be appended (though that might change) and dicts will be
+        # dealt with by calling this function again.
         else:
             # ints
             if isinstance(dict_1[key], int) and isinstance(dict_2[key], int):
@@ -756,6 +790,7 @@ def merge_dicts(dict_1, dict_2):
     #
     return new_dict
 
+
 class binarycDecoder(json.JSONDecoder):
     """
     Custom decoder to transform the numbers that are strings to actual floats
@@ -788,3 +823,14 @@ class binarycDecoder(json.JSONDecoder):
         else:
             return o
 
+def binaryc_json_serializer(obj):
+    """
+    Custom serializer for binary_c to use when functions are present in the dictionary
+    that we want to export.
+
+    Function objects will be turned into str representations of themselves
+    """
+
+    if inspect.isfunction(obj):
+        return str(obj)
+    return obj
diff --git a/binarycpython/utils/grid.py b/binarycpython/utils/grid.py
index ccd9ae19c..df736bfca 100644
--- a/binarycpython/utils/grid.py
+++ b/binarycpython/utils/grid.py
@@ -34,12 +34,16 @@ from binarycpython.utils.functions import (
     remove_file,
     filter_arg_dict,
     get_help_all,
+    return_binary_c_version_info,
+    binaryc_json_serializer,
+    verbose_print,
+    binarycDecoder,
+    merge_dicts
 )
 
 import binary_c_python_api
 
-
-# Todo-list
+# Tasks
 # TODO: add functionality to 'on-init' set arguments
 # TODO: add functionality to return the initial_abundance_hash
 # TODO: add functionality to return the isotope_hash
@@ -81,6 +85,10 @@ class Population:
         # Set main process id
         self.grid_options["main_pid"] = os.getpid()
 
+        # Set some memory dicts
+        self.persistent_data_memory_dict = {}
+
+
     ###################################################
     # Argument functions
     ###################################################
@@ -118,20 +126,21 @@ class Population:
         in the self.grid_options
 
         If neither of above is met; the key and the value get stored in a custom_options dict.
+
+        TODO: catch those parameter names that contain an %d
         """
 
         for key in kwargs:
             # Filter out keys for the bse_options
             if key in self.defaults.keys():
-                if self.grid_options["verbose"] > 0:
-                    print("adding: {}={} to BSE_options".format(key, kwargs[key]))
+                verbose_print("adding: {}={} to BSE_options".format(key, kwargs[key]), self.grid_options["verbose"], 1)
                 self.bse_options[key] = kwargs[key]
 
             # Filter out keys for the grid_options
             elif key in self.grid_options.keys():
-                if self.grid_options["verbose"] > 0:
-                    print("adding: {}={} to grid_options".format(key, kwargs[key]))
+                verbose_print("adding: {}={} to grid_options".format(key, kwargs[key]), self.grid_options["verbose"], 1)
                 self.grid_options[key] = kwargs[key]
+
             # The of the keys go into a custom_options dict
             else:
                 print(
@@ -159,8 +168,8 @@ class Population:
         # How its set up now is that as input you need to give --cmdline "metallicity=0.002"
         # Its checked if this exists and handled accordingly.
         if args.cmdline:
-            if self.grid_options["verbose"] > 0:
-                print("Found cmdline args. Parsing them now")
+            verbose_print("Found cmdline args. Parsing them now", self.grid_options["verbose"], 1)
+
             # Grab the input and split them up, while accepting only non-empty entries
             cmdline_args = args.cmdline
             split_args = [
@@ -208,17 +217,17 @@ class Population:
         pass
 
     def add_grid_variable(
-        self,
-        name,
-        longname,
-        valuerange,
-        resolution,
-        spacingfunc,
-        probdist,
-        dphasevol,
-        parameter_name,
-        precode=None,
-        condition=None,
+            self,
+            name,
+            longname,
+            valuerange,
+            resolution,
+            spacingfunc,
+            probdist,
+            dphasevol,
+            parameter_name,
+            precode=None,
+            condition=None,
     ):
         """spec
         Function to add grid variables to the grid_options.
@@ -276,8 +285,7 @@ class Population:
 
         # Load it into the grid_options
         self.grid_options["grid_variables"][grid_variable["name"]] = grid_variable
-        if self.grid_options["verbose"] > 0:
-            print("Added grid variable: {}".format(json.dumps(grid_variable, indent=4)))
+        verbose_print("Added grid variable: {}".format(json.dumps(grid_variable, indent=4)), self.grid_options["verbose"], 1)
 
     ###################################################
     # Return functions
@@ -318,11 +326,11 @@ class Population:
         return self.defaults
 
     def return_all_info(
-        self,
-        include_population_settings=True,
-        include_binary_c_defaults=True,
-        include_binary_c_version_info=True,
-        include_binary_c_help_all=True,
+            self,
+            include_population_settings=True,
+            include_binary_c_defaults=True,
+            include_binary_c_version_info=True,
+            include_binary_c_help_all=True,
     ):
         """
         Function that returns all the information about the population and binary_c
@@ -342,7 +350,7 @@ class Population:
             all_info["binary_c_defaults"] = binary_c_defaults
 
         if include_binary_c_version_info:
-            binary_c_version_info = self.return_binary_c_version_info(parsed=True)
+            binary_c_version_info = return_binary_c_version_info(parsed=True)
             all_info["binary_c_version_info"] = binary_c_version_info
 
         if include_binary_c_help_all:
@@ -352,13 +360,13 @@ class Population:
         return all_info
 
     def export_all_info(
-        self,
-        use_datadir=True,
-        outfile=None,
-        include_population_settings=True,
-        include_binary_c_defaults=True,
-        include_binary_c_version_info=True,
-        include_binary_c_help_all=True,
+            self,
+            use_datadir=True,
+            outfile=None,
+            include_population_settings=True,
+            include_binary_c_defaults=True,
+            include_binary_c_version_info=True,
+            include_binary_c_help_all=True,
     ):
         """
         Function that exports the all_info to a json file
@@ -382,18 +390,18 @@ class Population:
         # Copy dict
         all_info_cleaned = copy.deepcopy(all_info)
 
-        # Clean the all_info_dict: (i.e. transform the function objects to strings)
-        if all_info_cleaned.get("population_settings", None):
-            if all_info_cleaned["population_settings"]["grid_options"][
-                "parse_function"
-            ]:
-                all_info_cleaned["population_settings"]["grid_options"][
-                    "parse_function"
-                ] = str(
-                    all_info_cleaned["population_settings"]["grid_options"][
-                        "parse_function"
-                    ]
-                )
+        # # Clean the all_info_dict: (i.e. transform the function objects to strings)
+        # if all_info_cleaned.get("population_settings", None):
+        #     if all_info_cleaned["population_settings"]["grid_options"][
+        #             "parse_function"
+        #     ]:
+        #         all_info_cleaned["population_settings"]["grid_options"][
+        #             "parse_function"
+        #         ] = str(
+        #             all_info_cleaned["population_settings"]["grid_options"][
+        #                 "parse_function"
+        #             ]
+        #         )
 
         if use_datadir:
             if not self.custom_options.get("base_filename", None):
@@ -412,15 +420,15 @@ class Population:
                 self.custom_options["data_dir"], settings_name
             )
 
-            if self.grid_options["verbose"] > 0:
-                print("Writing settings to {}".format(settings_fullname))
+            verbose_print("Writing settings to {}".format(settings_fullname), self.grid_options["verbose"], 1)
             # if not outfile.endswith('json'):
             with open(settings_fullname, "w") as file:
-                file.write(json.dumps(all_info_cleaned, indent=4))
+                file.write(
+                    json.dumps(all_info_cleaned, indent=4, default=binaryc_json_serializer)
+                )
 
         else:
-            if self.grid_options["verbose"] > 0:
-                print("Writing settings to {}".format(outfile))
+            verbose_print("Writing settings to {}".format(outfile), self.grid_options["verbose"], 1)
             # if not outfile.endswith('json'):
             with open(outfile, "w") as file:
                 file.write(json.dumps(all_info_cleaned, indent=4))
@@ -432,9 +440,7 @@ class Population:
         """
 
         # C_logging_code gets priority of C_autogen_code
-        if self.grid_options["verbose"] > 0:
-            print("Creating and loading custom logging functionality")
-
+        verbose_print("Creating and loading custom logging functionality", self.grid_options["verbose"], 1)
         if self.grid_options["C_logging_code"]:
             # Generate entire shared lib code around logging lines
             custom_logging_code = binary_c_log_code(
@@ -470,6 +476,53 @@ class Population:
                 custom_logging_code, verbose=self.grid_options["verbose"]
             )
 
+    ###################################################
+    # Ensemble functions
+    ###################################################
+
+    def load_persistent_data_memory_dict(self):
+        """
+        Function that loads a set amount (amt_cores) of persistent data memory adresses to
+        pass to binary_c.
+
+        TODO: fix the function
+        """
+
+        for thread_nr in self.grid_options["amt_cores"]:
+            persistent_data_memaddr = binary_c_python_api.binary_c_return_persistent_data_memaddr()
+            self.persistent_data_memory_dict[thread_nr] = persistent_data_memaddr
+        verbose_print("Created the following dict with persistent memaddresses: {}".format(self.persistent_data_memory_dict), self.grid_options["verbosity"], 1)
+
+    def free_persistent_data_memory_and_combine_results(self):
+        """
+        Function that loads a set amount of persisten data memory adresses to
+        pass to binary_c.
+
+        TODO: fix the function
+        """
+
+        combined_ensemble_json = {}
+
+        for key in self.persistent_data_memory_dict:
+            persistent_data_memaddr = self.persistent_data_memory_dict[key]
+
+            verbose_print("Freeing {} (thread {})and merging output to combined dict".format(persistent_data_memaddr, key), self.grid_options["verbosity"], 1)
+
+            # Get the output and decode it correctly to get the numbers correct
+            ensemble_json_output = binary_c_python_api.binary_c_free_persistent_data_memaddr_and_return_json_output(persistent_data_memaddr)
+            parsed_json = json.loads(ensemble_json_output.splitlines()[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
+
+            # Combine the output with the main output
+            combined_ensemble_json = merge_dicts(combined_ensemble_json, parsed_json)
+
+        # Write results to file.
+        # TODO: Make sure everything is checked beforehand
+        full_output_filename = os.path.join(self.custom_options["data_dir"], self.custom_options["ensemble_output_name"])
+        verbose_print("Writing ensemble output to {}".format(full_output_filename), self.grid_options["verbosity"], 1)
+
+
+
+
     ###################################################
     # Evolution functions
     ###################################################
@@ -697,8 +750,8 @@ class Population:
         # Evolve systems: via grid_options one can choose to do this linearly, or
         # multiprocessing method.
         if (
-            self.grid_options["evolution_type"]
-            in self.grid_options["evolution_type_options"]
+                self.grid_options["evolution_type"]
+                in self.grid_options["evolution_type_options"]
         ):
             if self.grid_options["evolution_type"] == "mp":
                 self.evolve_population_mp()
@@ -725,11 +778,10 @@ class Population:
         Function to test the evolution of a system. Calls the api binding directly.
         """
 
-        if self.grid_options["verbose"] > 0:
-            print("running a single system as a test")
+        verbose_print("running a single system as a test", self.grid_options["verbose"], 1)
 
-        m1 = 15.0  # Msun
-        m2 = 14.0  # Msun
+        m_1 = 15.0  # Msun
+        m_2 = 14.0  # Msun
         separation = 0  # 0 = ignored, use period
         orbital_period = 4530.0  # days
         eccentricity = 0.0
@@ -737,8 +789,8 @@ class Population:
         max_evolution_time = 15000
         argstring = "binary_c M_1 {0:g} M_2 {1:g} separation {2:g} orbital_period {3:g}\
         eccentricity {4:g} metallicity {5:g} max_evolution_time {6:g}".format(
-            m1,
-            m2,
+            m_1,
+            m_2,
             separation,
             orbital_period,
             eccentricity,
@@ -779,8 +831,7 @@ class Population:
         Results in a generated file that contains a system_generator function.
         """
 
-        if self.grid_options["verbose"] > 0:
-            print("Generating grid code")
+        verbose_print("Generating grid code", self.grid_options["verbose"], 1)
 
         # Some local values
         code_string = ""
@@ -838,8 +889,8 @@ class Population:
         code_string += indent * depth + "# setting probability lists\n"
         # Prepare the probability
         for grid_variable_el in sorted(
-            self.grid_options["grid_variables"].items(),
-            key=lambda x: x[1]["grid_variable_number"],
+                self.grid_options["grid_variables"].items(),
+                key=lambda x: x[1]["grid_variable_number"],
         ):
             # Make probabilities dict
             grid_variable = grid_variable_el[1]
@@ -854,10 +905,10 @@ class Population:
         # Generate code
         print("Generating grid code")
         for loopnr, grid_variable_el in enumerate(
-            sorted(
-                self.grid_options["grid_variables"].items(),
-                key=lambda x: x[1]["grid_variable_number"],
-            )
+                sorted(
+                    self.grid_options["grid_variables"].items(),
+                    key=lambda x: x[1]["grid_variable_number"],
+                )
         ):
             print("Constructing/adding: {}".format(grid_variable_el[0]))
             grid_variable = grid_variable_el[1]
@@ -1100,11 +1151,11 @@ class Population:
         # this has to go in a reverse order:
         # Here comes the stuff that is put after the deepest nested part that calls returns stuff.
         for loopnr, grid_variable_el in enumerate(
-            sorted(
-                self.grid_options["grid_variables"].items(),
-                key=lambda x: x[1]["grid_variable_number"],
-                reverse=True,
-            )
+                sorted(
+                    self.grid_options["grid_variables"].items(),
+                    key=lambda x: x[1]["grid_variable_number"],
+                    reverse=True,
+                )
         ):
             grid_variable = grid_variable_el[1]
             code_string += indent * (depth + 1) + "#" * 40 + "\n"
@@ -1143,8 +1194,7 @@ class Population:
         # Stop of code generation. Here the code is saved and written
 
         # Save the gridcode to the grid_options
-        if self.grid_options["verbose"] > 0:
-            print("Saving grid code to grid_options")
+        verbose_print("Saving grid code to grid_options", self.grid_options["verbose"], 1)
 
         self.grid_options["code_string"] = code_string
 
@@ -1154,8 +1204,7 @@ class Population:
         )
         self.grid_options["gridcode_filename"] = gridcode_filename
 
-        if self.grid_options["verbose"] > 0:
-            print("Writing grid code to {}".format(gridcode_filename))
+        verbose_print("Writing grid code to {}".format(gridcode_filename), self.grid_options["verbose"], 1)
 
         with open(gridcode_filename, "w") as file:
             file.write(code_string)
@@ -1167,13 +1216,11 @@ class Population:
         """
 
         # Code to load the
-
-        if self.grid_options["verbose"] > 0:
-            print(
-                "Loading grid code function from {}".format(
-                    self.grid_options["gridcode_filename"]
-                )
-            )
+        verbose_print(
+            message="Loading grid code function from {}".format(
+                self.grid_options["gridcode_filename"]
+            ),
+            verbosity=self.grid_options["verbose"], minimal_verbosity=1)
 
         spec = importlib.util.spec_from_file_location(
             "binary_c_python_grid",
@@ -1185,8 +1232,7 @@ class Population:
 
         self.grid_options["system_generator"] = generator
 
-        if self.grid_options["verbose"] > 0:
-            print("Grid code loaded")
+        verbose_print("Grid code loaded", self.grid_options["verbose"], 1)
 
     def dry_run(self):
         """
@@ -1253,7 +1299,7 @@ class Population:
     ###################################################
 
     def write_binary_c_calls_to_file(
-        self, output_dir=None, output_filename=None, include_defaults=False
+            self, output_dir=None, output_filename=None, include_defaults=False
     ):
         """
         Function that loops over the gridcode and writes the generated parameters to a file.
@@ -1349,8 +1395,8 @@ class Population:
         """
 
         if evol_type == "single":
-            if self.grid_options["verbose"] > 0:
-                print("Cleaning up the custom logging stuff. type: single")
+            verbose_print("Cleaning up the custom logging stuff. type: single", self.grid_options["verbose"], 1)
+
             # TODO: Unset custom logging code
 
             # TODO: Unset function memory adress
@@ -1364,11 +1410,14 @@ class Population:
                 )
 
         if evol_type == "population":
-            if self.grid_options["verbose"] > 0:
-                print("Cleaning up the custom logging stuffs. type: population")
+            verbose_print("Cleaning up the custom logging stuffs. type: population", self.grid_options["verbose"], 1)
+
             # TODO: make sure that these also work. not fully sure if necessary tho.
             #   whether its a single file, or a dict of files/memaddresses
 
+        if evol_type == "MC":
+            pass
+
     def increment_probtot(self, prob):
         """
         Function to add to the total probability
diff --git a/tests/test_persistent_data.py b/tests/test_persistent_data.py
index e7c24cc02..9d20bad4c 100644
--- a/tests/test_persistent_data.py
+++ b/tests/test_persistent_data.py
@@ -9,7 +9,12 @@ import json
 import textwrap
 import binary_c_python_api
 
-from binarycpython.utils.functions import binarycDecoder, temp_dir
+from binarycpython.utils.functions import (
+    binarycDecoder, 
+    temp_dir,
+    inspect_dict,
+    merge_dicts,
+)
 
 TMP_DIR = temp_dir()
 os.makedirs(os.path.join(TMP_DIR, "test"),  exist_ok=True)
@@ -102,32 +107,42 @@ def test_adding_ensemble_output():
 
     m1 = 2  # Msun
     m2 = 0.1  # Msun
+    extra_mass = 10
 
     #############################################################################################
     # The 2 runs below use the ensemble but do not defer the output to anything else, so that the
     # results are returned directly after the run
 
     # Direct output commands
-    argstring_1 = return_argstring(m1=m1, m2=m2, ensemble_filter="STELLAR_TYPE_COUNTS", defer_ensemble=0)
-    argstring_2 = return_argstring(m1=m1+1, m2=m2, ensemble_filter="STELLAR_TYPE_COUNTS", defer_ensemble=0)
+    argstring_1 = return_argstring(m1=m1, m2=m2, 
+        ensemble_filter="STELLAR_TYPE_COUNTS", defer_ensemble=0)
+    argstring_2 = return_argstring(m1=m1 + extra_mass, m2=m2, 
+        ensemble_filter="STELLAR_TYPE_COUNTS", defer_ensemble=0)
 
     # Get outputs
     output_1 = binary_c_python_api.run_system(argstring=argstring_1)
     output_2 = binary_c_python_api.run_system(argstring=argstring_2)
 
-    ensemble_jsons_1 = [line for line in output_1.splitlines() if line.startswith("ENSEMBLE_JSON")]
-    ensemble_jsons_2 = [line for line in output_2.splitlines() if line.startswith("ENSEMBLE_JSON")]
+    test_1_ensemble_jsons_1 = [
+        line for line in output_1.splitlines() if line.startswith("ENSEMBLE_JSON")
+    ]
+    test_1_ensemble_jsons_2 = [
+        line for line in output_2.splitlines() if line.startswith("ENSEMBLE_JSON")
+    ]
 
-    json_1 = json.loads(ensemble_jsons_1[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
-    json_2 = json.loads(ensemble_jsons_2[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
+    test_1_json_1 = json.loads(
+        test_1_ensemble_jsons_1[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
+    test_1_json_2 = json.loads(
+        test_1_ensemble_jsons_2[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
 
-    # test_1_total_dict = SumDict(json_1)
-    # test_1_total_dict.merge(json_2)
+    test_1_merged_dict = merge_dicts(test_1_json_1, test_1_json_2)
 
-    with open(os.path.join(TMP_DIR, "test", "adding_json_1.json"), 'w') as f:
-        f.write(json.dumps(json_1, indent=4))
-    with open(os.path.join(TMP_DIR, "test", "adding_json_2.json"), 'w') as f:
-        f.write(json.dumps(json_2, indent=4))
+    with open(os.path.join(TMP_DIR, "test", "adding_json_1.json"), 'w') as file:
+        file.write(json.dumps(test_1_json_1, indent=4))
+    with open(os.path.join(TMP_DIR, "test", "adding_json_2.json"), 'w') as file:
+        file.write(json.dumps(test_1_json_2, indent=4))
+    with open(os.path.join(TMP_DIR, "test", "adding_json_merged.json"), 'w') as file:
+        file.write(json.dumps(test_1_json_2, indent=4))
 
     print("Single runs done\n")
 
@@ -137,31 +152,32 @@ def test_adding_ensemble_output():
     # have the output returned in that way
 
     # Deferred commands
-    argstring_1_deferred = return_argstring(m1=m1, m2=m2, ensemble_filter="STELLAR_TYPE_COUNTS", defer_ensemble=1)
-    argstring_2_deferred = return_argstring(m1=m1+1, m2=m2, ensemble_filter="STELLAR_TYPE_COUNTS", defer_ensemble=1)
+    argstring_1_deferred = return_argstring(
+        m1=m1, m2=m2, ensemble_filter="STELLAR_TYPE_COUNTS", defer_ensemble=1)
+    argstring_2_deferred = return_argstring(
+        m1=m1 + extra_mass, m2=m2, ensemble_filter="STELLAR_TYPE_COUNTS", defer_ensemble=1)
 
     # Get a memory location
-    persistent_data_memaddr = binary_c_python_api.return_persistent_data_memaddr()
+    test_2_persistent_data_memaddr = binary_c_python_api.return_persistent_data_memaddr()
 
     # Run the systems and defer the output each time
-    output_1_deferred = binary_c_python_api.run_system(
+    _ = binary_c_python_api.run_system(
         argstring=argstring_1_deferred,
-        persistent_data_memaddr=persistent_data_memaddr
+        persistent_data_memaddr=test_2_persistent_data_memaddr
     )
-    output_2_deferred = binary_c_python_api.run_system(
+    _ = binary_c_python_api.run_system(
         argstring=argstring_2_deferred,
-        persistent_data_memaddr=persistent_data_memaddr
+        persistent_data_memaddr=test_2_persistent_data_memaddr
     )
 
     # Have the persistent_memory adress be released and have the json outputted
-    output_total_deferred = binary_c_python_api.free_persistent_data_memaddr_and_return_json_output(persistent_data_memaddr)
-
-    ensemble_jsons_deferred = [line for line in output_total_deferred.splitlines() if line.startswith("ENSEMBLE_JSON")]
+    test_2_output = binary_c_python_api.free_persistent_data_memaddr_and_return_json_output(
+        test_2_persistent_data_memaddr)
+    test_2_ensemble_json = [line for line in test_2_output.splitlines() if line.startswith("ENSEMBLE_JSON")]
+    test_2_json = json.loads(test_2_ensemble_json[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
 
-    json_deferred = json.loads(ensemble_jsons_deferred[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
-
-    with open(os.path.join(TMP_DIR, "test", "adding_json_deferred.json"), 'w') as f:
-        f.write(json.dumps(json_deferred, indent=4))
+    with open(os.path.join(TMP_DIR, "test", "adding_json_deferred.json"), 'w') as file:
+        file.write(json.dumps(test_2_json, indent=4))
 
     print("Double deferred done\n")
 
@@ -170,27 +186,50 @@ def test_adding_ensemble_output():
     # Then the second one uses that memory to combine its results with, but doesn't defer the
     # data after that, so it will print it after the second run is done
 
-    persistent_data_memaddr_2 = binary_c_python_api.return_persistent_data_memaddr()
+    test_3_persistent_data_memaddr = binary_c_python_api.return_persistent_data_memaddr()
 
-    # Run the systems and defer the output once and the second time not, so that the second run automatically prints out the results
-    output_1_deferred = binary_c_python_api.run_system(
+    # Run the systems and defer the output once and the second time not, so that the second run
+    # automatically prints out the results
+    _ = binary_c_python_api.run_system(
         argstring=argstring_1_deferred,
-        persistent_data_memaddr=persistent_data_memaddr_2
+        persistent_data_memaddr=test_3_persistent_data_memaddr
     )
-    output_2_deferred_and_output = binary_c_python_api.run_system(
+    test_3_output_2 = binary_c_python_api.run_system(
         argstring=argstring_2,
-        persistent_data_memaddr=persistent_data_memaddr_2
+        persistent_data_memaddr=test_3_persistent_data_memaddr
     )
-
-    ensemble_jsons_deferred_and_output = [line for line in output_2_deferred_and_output.splitlines() if line.startswith("ENSEMBLE_JSON")]
-
-    json_deferred_and_output = json.loads(ensemble_jsons_deferred_and_output[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
+    test_3_ensemble_jsons = [
+        line for line in test_3_output_2.splitlines() if line.startswith("ENSEMBLE_JSON")
+    ]
+    test_3_json = json.loads(test_3_ensemble_jsons[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
 
     with open(os.path.join(TMP_DIR, "test", "adding_json_deferred_and_output.json"), 'w') as f:
-        f.write(json.dumps(json_deferred_and_output, indent=4))
+        f.write(json.dumps(test_3_json, indent=4))
 
     print("Single deferred done\n")
 
+    # 
+    assert_message_1 = """
+    The structure of the manually merged is not the same as the merged by double deferring
+    """
+    assert_message_2 = """
+    The structure of the manually merged is not the same as the merged by deferring once
+    and output on the second run
+    """
+
+    #
+    assert inspect_dict(test_1_merged_dict, print_structure=False) == inspect_dict(test_2_json, print_structure=False), assert_message_1
+    # assert inspect_dict(test_1_merged_dict, print_structure=False) == inspect_dict(test_3_json, print_structure=False), assert_message_2
+
+def combine_with_empty_json():
+    argstring_1 = return_argstring(defer_ensemble=0)
+    output_1 = binary_c_python_api.run_system(argstring=argstring_1)
+    ensemble_jsons_1 = [line for line in output_1.splitlines() if line.startswith("ENSEMBLE_JSON")]
+    json_1 = json.loads(ensemble_jsons_1[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
+
+    assert_message = "combining output json with empty dict should give same result as initial json"
+    assert merge_dicts(json_1, {}) == json_1, assert_message
+
 def test_free_and_json_output():
     """
     Function that tests the freeing of the memory adress and the output of the json
@@ -203,7 +242,7 @@ def test_free_and_json_output():
     argstring_1 = return_argstring(m1=m2, m2=m2, ensemble_filter="STELLAR_TYPE_COUNTS", defer_ensemble=1)
 
     # Get a memory adress:
-    persistent_data_memaddr = binary_c_python_api.return_persistent_data_memaddr("")
+    persistent_data_memaddr = binary_c_python_api.return_persistent_data_memaddr()
 
     # Evolve and defer output
     print("evolving")
@@ -219,10 +258,18 @@ def test_free_and_json_output():
     print("Output:")
     print(textwrap.indent(str(json_output_by_freeing), "\t"))
 
+
+    parsed_json = json.loads(json_output_by_freeing.splitlines()[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder) 
+    print(parsed_json)
+    # ensemble_jsons_1 = [line for line in output_1.splitlines() if line.startswith("ENSEMBLE_JSON")]
+    # json_1 = json.loads(ensemble_jsons_1[0][len("ENSEMBLE_JSON "):], cls=binarycDecoder)
+
+
 ####
 if __name__ == "__main__":
-    test_return_persistent_data_memaddr()
+    # test_return_persistent_data_memaddr()
     # test_passing_persistent_data_to_run_system()
     # test_full_ensemble_output()
     # test_adding_ensemble_output()
-    # test_free_and_json_output()
+    test_free_and_json_output()
+    # combine_with_empty_json()
-- 
GitLab