Skip to content
Snippets Groups Projects
functions.py 56.8 KiB
Newer Older
David Hendriks's avatar
David Hendriks committed
"""
Module containing most of the utility functions for the binarycpython package

Functions here are mostly functions used in other classes/functions, or
useful functions for the user

Tasks:
    - TODO: change all prints to verbose_prints
David Hendriks's avatar
David Hendriks committed
from io import StringIO
from typing import Union, Any
from collections import defaultdict

from binarycpython import _binary_c_bindings
import binarycpython.utils.moe_distefano_data as moe_distefano_data
########################################################
# Unsorted
########################################################
    """
    Function to return the size + a magnitude string
    """

    for x in ['bytes', 'KB', 'MB', 'GB', 'TB']:
        if size < 1024.0:
            return "%3.1f %s" % (size, x)
        size /= 1024.0
    return size

def get_size(obj, seen=None):
    """
    Recursively finds size of objects

    From https://github.com/bosswissam/pysize
    """

    size = sys.getsizeof(obj)
    if seen is None:
        seen = set()
    obj_id = id(obj)
    if obj_id in seen:
        return 0
    # Important mark as seen *before* entering recursion to gracefully handle
    # self-referential objects
    seen.add(obj_id)
    if isinstance(obj, dict):
        size += sum([get_size(v, seen) for v in obj.values()])
        size += sum([get_size(k, seen) for k in obj.keys()])
    elif hasattr(obj, '__dict__'):
        size += get_size(obj.__dict__, seen)
    elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes, bytearray)):
        size += sum([get_size(i, seen) for i in obj])
    return size
David Hendriks's avatar
David Hendriks committed

def subtract_dicts(dict_1: dict, dict_2: dict) -> dict:
    """
    Function to subtract two dictionaries.

    Only allows values to be either a dict or a numerical type

    For the overlapping keys (key name present in both dicts):
        When the keys are of the same type:
            - If the types are of numerical type: subtract the value at dict 2 from dict 1.
            - If the types are both dictionaries: call this function with the subdicts

        WHen the keys are not of the same type:
            - if the keys are all of numerical types

    For the unique keys:
        - if the key is from dict 1: adds the value to the new dict (be it numerical value or dict)
        - If the key is from dict 2: Adds the negative of its value in case of numerical type.
            if the type is a dict, the result of subtract_dicts({}, dict_2[key]) will be set

    If the result is 0, the key will be removed from the resulting dict.
    If that results in an empty dict, the dict will be removed too.

    Args:
        dict_1: first dictionary
        dict_2: second dictionary

    Returns:
        Subtracted dictionary
    """

    # Set up new dict
    new_dict = {}

    # Define allowed numerical types
    ALLOWED_NUMERICAL_TYPES = (float, int, np.float64)

    #
    keys_1 = dict_1.keys()
    keys_2 = dict_2.keys()

    # Find overlapping keys of both dicts
    overlapping_keys = set(keys_1).intersection(set(keys_2))

    # Find the keys that are unique
    unique_to_dict_1 = set(keys_1).difference(set(keys_2))
    unique_to_dict_2 = set(keys_2).difference(set(keys_1))

    # Add the unique keys to the new dict
    for key in unique_to_dict_1:
        # If these items are numerical types
        if isinstance(dict_1[key], ALLOWED_NUMERICAL_TYPES):
            new_dict[key] = dict_1[key]
            if new_dict[key] == 0:
                del new_dict[key]

        # Else, to be safe we should deepcopy them
        elif isinstance(dict_1[key], dict):
            copy_dict = copy.deepcopy(dict_1[key])
            new_dict[key] = copy_dict
        else:
            msg = "Error: using unsupported type for key {}: {}".format(
                key, type(dict_1[key])
            )
            print(msg)
            raise ValueError(msg)

    # Add the unique keys to the new dict
    for key in unique_to_dict_2:
        # If these items are numerical type, we should add the negative of the value
        if isinstance(dict_2[key], ALLOWED_NUMERICAL_TYPES):
            new_dict[key] = -dict_2[key]
            if new_dict[key] == 0:
                del new_dict[key]

        # Else we should place the negative of that dictionary in the new place
        elif isinstance(dict_2[key], dict):
            new_dict[key] = subtract_dicts({}, dict_2[key])
        else:
            msg = "Error: using unsupported type for key {}: {}".format(
                key, type(dict_2[key])
            )
            print(msg)
            raise ValueError(msg)

    # Go over the common keys:
    for key in overlapping_keys:

        # See whether the types are actually the same
        if not type(dict_1[key]) is type(dict_2[key]):
            # Exceptions:
            if (type(dict_1[key]) in ALLOWED_NUMERICAL_TYPES) and (
                type(dict_2[key]) in ALLOWED_NUMERICAL_TYPES
            ):
                # We can safely subtract the values since they are all numeric
                new_dict[key] = dict_1[key] - dict_2[key]
                if new_dict[key] == 0:
                    del new_dict[key]

            else:
                print(
                    "Error key: {} value: {} type: {} and key: {} value: {} type: {} are not of the same type and cannot be merged".format(
                        key,
                        dict_1[key],
                        type(dict_1[key]),
                        key,
                        dict_2[key],
                        type(dict_2[key]),
                    )
                )
                raise ValueError

        # This is where the keys are the same
        else:
            # If these items are numeric types
            if isinstance(dict_1[key], ALLOWED_NUMERICAL_TYPES):
                new_dict[key] = dict_1[key] - dict_2[key]

                # Remove entry if the value is 0
                if new_dict[key] == 0:
                    del new_dict[key]

            # Else, to be safe we should deepcopy them
            elif isinstance(dict_1[key], dict):
                new_dict[key] = subtract_dicts(dict_1[key], dict_2[key])

                # Remove entry if it results in an empty dict
                # TODO: write test to prevent empty dicts from showing up
                if not new_dict[key]:
                    del new_dict[key]
            else:
                msg = "Error: using unsupported type for key {}: {}".format(
                    key, type(dict_2[key])
                )
                print(msg)
                raise ValueError(msg)

    #
    return new_dict


def get_moe_distefano_dataset(options):
    """
    Function to get the default moe and Distefano dataset or accept a userinput.
    """

    if not options.get("file", None):
        print("Using the default Moe and de Stefano 2017 datafile")

        json_data = copy.deepcopy(moe_distefano_data.moe_distefano_2017_data)

    else:
        if not os.path.isfile(options["file"]):
            print(
                "The provided 'file' Moe and de Stefano JSON file does not seem to exist at {}".format(
                    options["file"]
                )
            )
            raise ValueError
        if not options["file"].endswith(".json"):
            print("Provided filename does not end with .json")

        else:
            # Read input data and Clean up the data if there are whitespaces around the keys
            with open(options["file"], "r") as data_filehandle:
                datafile_data = data_filehandle.read()
            datafile_data = datafile_data.replace('" ', '"')
            datafile_data = datafile_data.replace(' "', '"')
            datafile_data = datafile_data.replace(' "', '"')
            json_data = json.loads(datafile_data)

    return json_data


David Hendriks's avatar
David Hendriks committed
def imports():
    for name, val in globals().items():
        if isinstance(val, types.ModuleType):
            yield val.__name__


class catchtime(object):
David Hendriks's avatar
David Hendriks committed
    """
    Context manager to calculate time spent
    """

    def __enter__(self):
David Hendriks's avatar
David Hendriks committed
        """On entry we start the clock"""
        self.t = time.clock()
        return self

    def __exit__(self, type, value, traceback):
David Hendriks's avatar
David Hendriks committed
        """On exit we stop the clock and measure the time spent"""
        self.t = time.clock() - self.t
        print("Took {}s".format(self.t))

David Hendriks's avatar
David Hendriks committed
    """
    Function to tell whether object is a capsule
    """

David Hendriks's avatar
David Hendriks committed
    return t.__module__ == "builtins" and t.__name__ == "PyCapsule"
class Capturing(list):
David Hendriks's avatar
David Hendriks committed
    """
    Context manager to capture output and store it
    """

David Hendriks's avatar
David Hendriks committed
        """On entry we capture the stdout output"""

        self._stdout = sys.stdout
        sys.stdout = self._stringio = StringIO()
        return self
David Hendriks's avatar
David Hendriks committed

    def __exit__(self, *args):
David Hendriks's avatar
David Hendriks committed
        """On exit we release the capture again"""

        self.extend(self._stringio.getvalue().splitlines())
David Hendriks's avatar
David Hendriks committed
        del self._stringio  # free up some memory
        sys.stdout = self._stdout

David Hendriks's avatar
David Hendriks committed

########################################################
# utility functions
########################################################
def verbose_print(message: str, verbosity: int, minimal_verbosity: int) -> None:
    """
    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
    Args:
        message: message to print
        verbosity: current verbosity level
        minimal_verbosity: threshold verbosity above which to print
def remove_file(file: str, verbosity: int = 0) -> None:
    """
    Function to remove files but with verbosity

    Args:
        file: full filepath to the file that will be removed.
        verbosity: current verbosity level (Optional)
        the path of a subdirectory called binary_c_python in the TMP of the filesystem
            verbose_print(
                "This path ({}) is a directory, not a file".format(file), verbosity, 0
            )
            verbose_print("Removed {}".format(file), verbosity, 1)
        except FileNotFoundError as inst:
            print("Error while deleting file {}: {}".format(file, inst))
        verbose_print(
            "File/directory {} doesn't exist. Can't remove it.".format(file),
            verbosity,
            1,
        )
David Hendriks's avatar
David Hendriks committed

    Function to create directory within the TMP directory of the filesystem

    Makes use of os.makedirs exist_ok which requires python 3.2+
    Args:
        function arguments: str input where each next input will be a child of the previous full_path. e.g. temp_dir('tests', 'grid') will become '/tmp/binary_c_python/tests/grid'

        the path of a subdirectory called binary_c_python in the TMP of the filesystem
    """

    tmp_dir = tempfile.gettempdir()
    path = os.path.join(tmp_dir, "binary_c_python")

    # loop over the other paths if there are any:
    if args:
        for extra_dir in args:
            path = os.path.join(path, extra_dir)

def create_hdf5(data_dir: str, name: str) -> None:
    Function to create an hdf5 file from the contents of a directory:
     - settings file is selected by checking on files ending on settings
     - data files are selected by checking on files ending with .dat

    TODO: fix missing settingsfiles
    Args:
        data_dir: directory containing the data files and settings file
        name: name of hdf5file.

    # Make HDF5:
    # Create the file
    hdf5_filename = os.path.join(data_dir, "{}".format(name))
    print("Creating {}".format(hdf5_filename))
    hdf5_file = h5py.File(hdf5_filename, "w")

    # Get content of data_dir
    content_data_dir = os.listdir(data_dir)

    # Settings
    if any([file.endswith("_settings.json") for file in content_data_dir]):
        print("Adding settings to HDF5 file")
        settings_file = os.path.join(
            data_dir,
            [file for file in content_data_dir if file.endswith("_settings.json")][0],
        )

        with open(settings_file, "r") as settings_file:
            settings_json = json.load(settings_file)

        # Create settings group
        settings_grp = hdf5_file.create_group("settings")

        # Write version_string to settings_group
        settings_grp.create_dataset("used_settings", data=json.dumps(settings_json))

    # Get data files
    data_files = [el for el in content_data_dir if el.endswith(".dat")]
    if data_files:
        print("Adding data to HDF5 file")

        # Create the data group
        data_grp = hdf5_file.create_group("data")

        # Write the data to the file:
        # Make sure:
        for data_file in data_files:
            # filename stuff
            filename = data_file
            full_path = os.path.join(data_dir, filename)
            base_name = os.path.splitext(os.path.basename(filename))[0]

            # Get header info
            header_name = "{base_name}_header".format(base_name=base_name)
            data_headers = np.genfromtxt(full_path, dtype="str", max_rows=1)
            data_headers = np.char.encode(data_headers)
            data_grp.create_dataset(header_name, data=data_headers)

            # Add data
            data = np.loadtxt(full_path, skiprows=1)
            data_grp.create_dataset(base_name, data=data)

        hdf5_file.close()

########################################################
# version_info functions
########################################################
def return_binary_c_version_info(parsed: bool = False) -> Union[str, dict]:
    Function that returns the version information of binary_c. This function calls the function
    _binary_c_bindings.return_version_info()

    Args:
        parsed: Boolean flag whether to parse the version_info output of binary_c. default = False

    Returns:
        Either the raw string of binary_c or a parsed version of this in the form of a nested
        dictionary
    found_prev = False
    if "BINARY_C_MACRO_HEADER" in os.environ:
        # the envvar is already present. lets save that and put that back later
        found_prev = True
        prev_value = os.environ["BINARY_C_MACRO_HEADER"]

    os.environ["BINARY_C_MACRO_HEADER"] = "macroxyz"

    # Get version_info
    version_info = _binary_c_bindings.return_version_info().strip()
    # parse if wanted
    if parsed:
        version_info = parse_binary_c_version_info(version_info)

    # delete value
    del os.environ["BINARY_C_MACRO_HEADER"]
    # put stuff back if we found a previous one
    if found_prev:
        os.environ["BINARY_C_MACRO_HEADER"] = prev_value

    return version_info
def parse_binary_c_version_info(version_info_string: str) -> dict:
    Function that parses the binary_c version info. Long function with a lot of branches

David Hendriks's avatar
David Hendriks committed
    TODO: fix this function. stuff is missing: isotopes, macros, nucleosynthesis_sources

    Args:
        version_info_string: raw output of version_info call to binary_c

    Returns:
        Parsed version of the version info, which is a dictionary containing the keys: 'isotopes' for isotope info, 'argpairs' for argument pair info (TODO: explain), 'ensembles' for ensemble settings/info, 'macros' for macros, 'elements' for atomic element info, 'DTlimit' for (TODO: explain), 'nucleosynthesis_sources' for nucleosynthesis sources, and 'miscellaneous' for all those that were not caught by the previous groups. 'git_branch', 'git_build', 'revision' and 'email' are also keys, but its clear what those contain.
    # Clean data and put in correct shape
    splitted = version_info_string.strip().splitlines()
    cleaned = {el.strip() for el in splitted if not el == ""}
    ##########################
    # Network:
David Hendriks's avatar
David Hendriks committed
    # Split off all the networks and parse the info.

    networks = {el for el in cleaned if el.startswith("Network ")}
    cleaned = cleaned - networks

    networks_dict = {}
    for el in networks:
        network_dict = {}
        split_info = el.split("Network ")[-1].strip().split("==")

        network_number = int(split_info[0])
David Hendriks's avatar
David Hendriks committed
        network_dict["network_number"] = network_number

        network_info_split = split_info[1].split(" is ")

        shortname = network_info_split[0].strip()
David Hendriks's avatar
David Hendriks committed
        network_dict["shortname"] = shortname

        if not network_info_split[1].strip().startswith(":"):
            network_split_info_extra = network_info_split[1].strip().split(":")

            longname = network_split_info_extra[0].strip()
David Hendriks's avatar
David Hendriks committed
            network_dict["longname"] = longname
David Hendriks's avatar
David Hendriks committed
            implementation = (
                network_split_info_extra[1].strip().replace("implemented in", "")
            )
David Hendriks's avatar
David Hendriks committed
                network_dict["implemented_in"] = implementation.strip().split()

        networks_dict[network_number] = copy.deepcopy(network_dict)
    version_info_dict["networks"] = networks_dict if networks_dict else None

    ##########################
    # Isotopes:
    # Split off
    isotopes = {el for el in cleaned if el.startswith("Isotope ")}
    cleaned = cleaned - isotopes

    isotope_dict = {}
    for el in isotopes:
        split_info = el.split("Isotope ")[-1].strip().split(" is ")

        isotope_info = split_info[-1]
        name = isotope_info.split(" ")[0].strip()
        mass_g = float(
            isotope_info.split(",")[0].split("(")[1].split("=")[-1][:-2].strip()
        )
        mass_amu = float(
            isotope_info.split(",")[0].split("(")[-1].split("=")[-1].strip()
        )
        mass_mev = float(
            isotope_info.split(",")[-3].split("=")[-1].replace(")", "").strip()
        )
        A = int(isotope_info.split(",")[-1].strip().split("=")[-1].replace(")", ""))
        Z = int(isotope_info.split(",")[-2].strip().split("=")[-1])

        #
        isotope_dict[int(split_info[0])] = {
            "name": name,
            "Z": Z,
            "A": A,
            "mass_mev": mass_mev,
            "mass_g": mass_g,
            "mass_amu": mass_amu,
        }
David Hendriks's avatar
David Hendriks committed
    version_info_dict["isotopes"] = isotope_dict if isotope_dict else None

    ##########################
    # Argpairs:
    # Split off
    argpairs = set([el for el in cleaned if el.startswith("ArgPair")])
    cleaned = cleaned - argpairs
    argpair_dict = {}
    for el in sorted(argpairs):
        split_info = el.split("ArgPair ")[-1].split(" ")

        if not argpair_dict.get(split_info[0], None):
            argpair_dict[split_info[0]] = {split_info[1]: split_info[2]}
        else:
            argpair_dict[split_info[0]][split_info[1]] = split_info[2]

David Hendriks's avatar
David Hendriks committed
    version_info_dict["argpairs"] = argpair_dict if argpair_dict else None

    ##########################
    # ensembles:
    # Split off
    ensembles = {el for el in cleaned if el.startswith("Ensemble")}
    cleaned = cleaned - ensembles

    ensemble_dict = {}
    for el in ensembles:
        split_info = el.split("Ensemble ")[-1].split(" is ")
        if len(split_info) > 1:
            ensemble_dict[int(split_info[0])] = split_info[-1]
David Hendriks's avatar
David Hendriks committed
    version_info_dict["ensembles"] = ensemble_dict if ensemble_dict else None

    ##########################
    # macros:
    # Split off
    macros = {el for el in cleaned if el.startswith("macroxyz")}
    cleaned = cleaned - macros

    param_type_dict = {
        "STRING": str,
        "FLOAT": float,
        "MACRO": str,
        "INT": int,
        "LONG_INT": int,
David Hendriks's avatar
David Hendriks committed
        "UINT": int,
    }

    macros_dict = {}
    for el in macros:
        split_info = el.split("macroxyz ")[-1].split(" : ")
        param_type = split_info[0]

        new_split = "".join(split_info[1:]).split(" is ")
        param_name = new_split[0]
        param_value = " is ".join(new_split[1:])
David Hendriks's avatar
David Hendriks committed

        # Sometimes the macros have extra information behind it. Needs an update in outputting by binary_c
        try:
            macros_dict[param_name] = param_type_dict[param_type](param_value)
        except ValueError:
            macros_dict[param_name] = str(param_value)
David Hendriks's avatar
David Hendriks committed
    version_info_dict["macros"] = macros_dict if macros_dict else None

    ##########################
    # Elements:
    # Split off:
    elements = {el for el in cleaned if el.startswith("Element")}
    cleaned = cleaned - elements

    # Fill dict:
    elements_dict = {}
    for el in elements:
        split_info = el.split("Element ")[-1].split(" : ")
        name_info = split_info[0].split(" is ")

        # get isotope info
        isotopes = {}
        if not split_info[-1][0] == "0":
            isotope_string = split_info[-1].split(" = ")[-1]
            isotopes = {
                int(split_isotope.split("=")[0]): split_isotope.split("=")[1]
                for split_isotope in isotope_string.split(" ")
            }
        elements_dict[int(name_info[0])] = {
            "name": name_info[-1],
            "atomic_number": int(name_info[0]),
            "amt_isotopes": len(isotopes),
            "isotopes": isotopes,
        }
David Hendriks's avatar
David Hendriks committed
    version_info_dict["elements"] = elements_dict if elements_dict else None

    ##########################
    # dt_limits:
    # split off
    dt_limits = {el for el in cleaned if el.startswith("DTlimit")}
    cleaned = cleaned - dt_limits

    # Fill dict
    dt_limits_dict = {}
    for el in dt_limits:
        split_info = el.split("DTlimit ")[-1].split(" : ")
        dt_limits_dict[split_info[1].strip()] = {
            "index": int(split_info[0]),
            "value": float(split_info[-1]),
        }
David Hendriks's avatar
David Hendriks committed
    version_info_dict["dt_limits"] = dt_limits_dict if dt_limits_dict else None

    ##########################
    # Nucleosynthesis sources:
    # Split off
    nucsyn_sources = {el for el in cleaned if el.startswith("Nucleosynthesis")}
    cleaned = cleaned - nucsyn_sources

    # Fill dict
    nucsyn_sources_dict = {}
    for el in nucsyn_sources:
        split_info = el.split("Nucleosynthesis source")[-1].strip().split(" is ")
        nucsyn_sources_dict[int(split_info[0])] = split_info[-1]

David Hendriks's avatar
David Hendriks committed
    version_info_dict["nucleosynthesis_sources"] = (
        nucsyn_sources_dict if nucsyn_sources_dict else None
    )

    ##########################
    # miscellaneous:
    # All those that I didnt catch with the above filters. Could try to get some more out though.
    # TODO: filter a bit more.

    misc_dict = {}

    # Filter out git revision
    git_revision = [el for el in cleaned if el.startswith("git revision")]
    misc_dict["git_revision"] = (
        git_revision[0].split("git revision ")[-1].replace('"', "")
    )
    cleaned = cleaned - set(git_revision)

    # filter out git url
    git_url = [el for el in cleaned if el.startswith("git URL")]
    misc_dict["git_url"] = git_url[0].split("git URL ")[-1].replace('"', "")
    cleaned = cleaned - set(git_url)

    # filter out version
    version = [el for el in cleaned if el.startswith("Version")]
    misc_dict["version"] = str(version[0].split("Version ")[-1])
    cleaned = cleaned - set(version)
    git_branch = [el for el in cleaned if el.startswith("git branch")]
    misc_dict["git_branch"] = git_branch[0].split("git branch ")[-1].replace('"', "")
David Hendriks's avatar
David Hendriks committed
    cleaned = cleaned - set(git_branch)
    build = [el for el in cleaned if el.startswith("Build")]
    misc_dict["build"] = build[0].split("Build: ")[-1].replace('"', "")
David Hendriks's avatar
David Hendriks committed
    cleaned = cleaned - set(build)
    email = [el for el in cleaned if el.startswith("Email")]
    misc_dict["email"] = email[0].split("Email ")[-1].split(",")
David Hendriks's avatar
David Hendriks committed
    cleaned = cleaned - set(email)
David Hendriks's avatar
David Hendriks committed
    other_items = set([el for el in cleaned if " is " in el])
David Hendriks's avatar
David Hendriks committed
    cleaned = cleaned - other_items
David Hendriks's avatar
David Hendriks committed
    for el in other_items:
David Hendriks's avatar
David Hendriks committed
        split = el.split(" is ")
David Hendriks's avatar
David Hendriks committed
        key = split[0].strip()
David Hendriks's avatar
David Hendriks committed
        val = " is ".join(split[1:]).strip()
David Hendriks's avatar
David Hendriks committed
        misc_dict[key] = val

    misc_dict["uncaught"] = list(cleaned)
David Hendriks's avatar
David Hendriks committed

    version_info_dict["miscellaneous"] = misc_dict if misc_dict else None
########################################################
# binary_c output functions
########################################################
def output_lines(output: str) -> list:
    Function that outputs the lines that were recieved from the binary_c run, but now as an iterator.

    Args:
        output: raw binary_c output

    Returns:
        Iterator over the lines of the binary_c output
    if output:
        return output.splitlines()
David Hendriks's avatar
David Hendriks committed

def example_parse_output(output: str, selected_header: str) -> dict:
    Function that parses output of binary_c. This version serves as an example and is quite
    detailed. Custom functions can be easier:
    This function works in two cases:
    if the caught line contains output like 'example_header time=12.32 mass=0.94 ..'
    or if the line contains output like 'example_header 12.32 0.94'
    Please dont the two cases.
    You can give a 'selected_header' to catch any line that starts with that.
    Then the values will be put into a dictionary.
    Tasks:
        - TODO: Think about exporting to numpy array or pandas instead of a defaultdict
        - TODO: rethink whether this function is necessary at all
        - TODO: check this function again

    Args:
        output: binary_c output string
        selected_header: string header of the output (the start of the line that you want to 
            process)
David Hendriks's avatar
David Hendriks committed

        dictionary containing parameters as keys and lists for the values
    # split output on newlines
    for line in output.split("\n"):
        # Skip any blank lines
        if not line == "":
            split_line = line.split()
            # Select parts
            header = split_line[0]
            values_list = split_line[1:]
            # print(values_list)
            # Catch line starting with selected header
            if header == selected_header:
                # Check if the line contains '=' symbols:
                value_dict = {}
                if all("=" in value for value in values_list):
                    for value in values_list:
                        key, val = value.split("=")
                        value_dict[key.strip()] = val.strip()
                    value_dicts.append(value_dict)
                else:
                    if any("=" in value for value in values_list):
                        raise ValueError(
                            "Caught line contains some = symbols but not \
                            all of them do. aborting run"
                        )
                    for j, val in enumerate(values_list):
                        value_dict[j] = val
                    value_dicts.append(value_dict)
    if len(value_dicts) == 0:
        print(
            "Sorry, didnt find any line matching your header {}".format(selected_header)
    # Construct final dict.
    final_values_dict = defaultdict(list)
    for value_dict in value_dicts:
        for key in keys:
            final_values_dict[key].append(value_dict[key])
########################################################
# Argument and default value functions
########################################################
def get_defaults(filter_values: bool = False) -> dict:
    """
    Function that calls the binaryc get args function and cast it into a dictionary.
    Args:
        filter_values: whether to filter out NULL and Function defaults.

    Returns:
        dictionary containing the parameter name as key and the parameter default as value
    default_output = _binary_c_bindings.return_arglines()
    for default in default_output.split("\n"):
        if not default in ["__ARG_BEGIN", "__ARG_END", ""]:
            key, value = default.split(" = ")
            default_dict[key] = value
    if filter_values:
        default_dict = filter_arg_dict(default_dict)
def get_arg_keys() -> list:
    Function that return the list of possible keys to give in the arg string.
    This function calls get_defaults()
        list of all the parameters that binary_c accepts (and has default values for, since
        we call get_defaults())
    return list(get_defaults().keys())
def filter_arg_dict(arg_dict: dict) -> dict:
    """
    Function to filter out keys that contain values included in ['NULL', 'Function', '']
    This function is called by get_defaults()

    Args:
        arg_dict: dictionary containing the argument + default keypairs of binary_c

    Returns:
        filtered dictionary (pairs with NULL and Function values are removed)
    for key in old_dict.keys():
        if not old_dict[key] in ["NULL", "Function"]:
            if not old_dict[key] == "":
                new_dict[key] = old_dict[key]

    return new_dict

def create_arg_string(
    arg_dict: dict, sort: bool = False, filter_values: bool = False
) -> str:
    Function that creates the arg string for binary_c. Takes a dictionary containing the arguments
    and writes them to a string
    This string is missing the 'binary_c ' at the start.
        sort: (optional, default = False) Boolean whether to sort the order of the keys.
        filter_values: (optional, default = False) filters the input dict on keys that have NULL or `function` as value.
        The string built up by combining all the key + value's.
    arg_string = ""
    # Whether to filter the arguments
        arg_dict = filter_arg_dict(arg_dict)
    keys = sorted(arg_dict.keys()) if sort else arg_dict.keys()
        arg_string += "{key} {value} ".format(key=key, value=arg_dict[key])
    arg_string = arg_string.strip()
    return arg_string

########################################################
# Help functions
########################################################
def get_help(
    param_name: str = "", print_help: bool = True, fail_silently: bool = False
) -> Union[dict, None]:
    Function that returns the help info for a given parameter, by interfacing with binary_c

    Will check whether it is a valid parameter.

    Binary_c will output things in the following order;
    - Did you mean?
    - binary_c help for variable
    This function reads out that structure and catches the different components of this output
    Tasks:
        - TODO: consider not returning None, but return empty dict
        param_name: name of the parameter that you want info from. Will get checked whether its a
            valid parameter name
        print_help: (optional, default = True) whether to print out the help information
        fail_silently: (optional, default = False) Whether to print the errors raised if the
        parameter isn't valid
        Dictionary containing the help info. This dictionary contains 'parameter_name',
        'parameter_value_input_type', 'description', optionally 'macros'
    available_arg_keys = get_arg_keys()

        print(
            "Please set the param_name to any of the following:\n {}".format(
                sorted(available_arg_keys)
            )
        )