Source code for simcado.commands

# """
# This module contains classes which control how a simulation is run
#
# Module Summary
# --------------
# UserCommands is essentially a dictionary that holds all the variables that
# the user may wish to change. It also has some set variables like ``pix_res``
# that can be accessed directly, instead of from the dictionary.
#
# UserCommands is imported directly into the simmetis package and is accessible
# from the main package - ``simmetis.UserCommands``
#
# Classes
# -------
# ``UserCommands(filename, sim_data_dir=<path to data files>)``
#
# Routines
# --------
#
# * ``dump_defaults(filename="./", selection="freq")``
# * ``dump_chip_layout(dir="./")``
#
#
# See Also
# --------
# Classes that require a ``UserCommands`` object directly include:
#
# * ``Detector``
# * ``OpticalTrain``
#
#
# Notes
# -----
#
# References
# ----------
#
# Examples
# --------
# By default ``UserCommands`` contains the parameters needed to generate the MICADO
# optical train:
#
#     >>> my_cmds = simcado.UserCommands()
#
# To list the keywords that are available:
#
#     >>> my_cmds.keys()
#     ...
#
#
# The UserCommands object also contains smaller dictionaries for each category of
# keywords - e.g. for the keywords for the instrument:
#
#     >>> my_cmds.inst
#     ...
#
#
# """

import os
import shutil
import warnings
import logging

from collections import OrderedDict

import numpy as np
import astropy.io.ascii as ioascii

import simcado as sim
from . import spectral as sc
from .utils import __pkg_dir__, atmospheric_refraction, find_file
from .psf import PSFCube

#__all__ = []
__all__ = ["UserCommands", "dump_defaults", "dump_chip_layout",
           "dump_mirror_config", "read_config", "update_config"]


[docs]class UserCommands(object): """ An extended dictionary with the parameters needed for running a simulation Extended Summary ---------------- A :class:`.UserCommands` object contains a dictionary which holds all the keywords from the ``default.config`` file. It also has attributes which represent the frequently used variables, i.e. ``pix_res``, ``lam_bin_edges``, ``exptime``, etc ``<UserCommands>.cmds`` is a dictionary that holds all the variables the user may wish to change. It also has some set variables like ``<UserCommands>.pix_res`` that can be accessed directly, instead of from the dictionary. ``UserCommands`` is imported directly into the simcado package and is accessable from the main package - ``simcado.UserCommands`` If UserCommands is called without any arguments, the default values for MICADO and the E-ELT are used. Parameters ---------- filename : str, optional path to the user's .config file Attributes ---------- Internal dictionaries cmds : dict (collections.OrderedDict) the dictionary which holds all the keyword-value pairs needed for running a simualtion obs : dict (collections.OrderedDict) parameters about the observation sim : dict (collections.OrderedDict) parameters about the simualtion atmo : dict (collections.OrderedDict) parameters about the atmosphere scope : dict (collections.OrderedDict) parameters about the telescope inst : dict (collections.OrderedDict) parameters about the instrument fpa : dict (collections.OrderedDict) parameters about the detector array (FPA - Focal Plane Array) hxrg : dict (collections.OrderedDict) parameters about the chip noise (HxRG - HAWAII 4RG chip series) Attributes pertaining to the purely spectral data sets (e.g. transmission curves, stellar spectra, etc) lam : np.ndarray a vector containing the centres of the wavelength bins used when resampling the spectra or transmission curves lam_res : float [um] the resolution of the ``lam`` Attributes pertaining to the binning in spectral space for which different PSFs need to be used lam_psf_res : float [um] the spectal "distance" between layers - i.e. width of the bins lam_bin_edges : array-like [um] the edge of the spectral bin for each layer lam_bin_centers : array-like [um] the centres of the spectral bin for each layer Attributes pertaining to the binning in the spatial plane pix_res : float [arcsec] the internal (oversampled) spatial resolution of the simulation fpa_res : float [arcsec] the field of view of the individual pixels on the detector General attributes verbose : bool Flag for printing intermediate results to the screen (default=True) exptime : float [s] exposure time of a single DIT diameter : float [m] outer diamter of the primary aperture (i.e. M1) area : float [m^2] effective area of the primary aperture (i.e. M1) filter : str [BVRIzYJHKKs,user] filter used for the observation Methods ------- update(new_dict) updates the current ``UserCommands`` object from another dict-like object keys() returns the keys in the ``UserCommands.cmds`` dictionary values() returns the values in the ``UserCommands.cmds`` dictionary See Also -------- :class:`simcado.detector.Detector`, :class:`simcado.optics.OpticalTrain` Examples -------- By default ``UserCommands`` contains the parameters needed to generate the MICADO optical train: >>> import simcado >>> my_cmds = simcado.UserCommands() To list the keywords that are available: >>> my_cmds.keys() ... The ``UserCommands`` object also contains smaller dictionaries for each category of keywords - e.g. for the keywords describing the instrument: >>> my_cmds.inst ... """ def __init__(self, filename=None, sim_data_dir=None): """ Create an extended dictionary of simulation parameters Parameters ---------- filename : str, optional path to the user's .config file sim_data_dir : str, optional path to directory where instrument data are stored SimCADO needs to know where the instrument-specific data files are stored. This can be specified in the config file (keyword `SIM_DATA_DIR`) or in the call to `UserCommands`. The default configuration file does not include a valid sim_data_dir, hence at least one of the parameters `filename` or `sim_data_dir` must be provided. """ logging.info("UserCommands object created") self.pkg_dir = __pkg_dir__ default = os.path.join(self.pkg_dir, "data", "default.config") # read in the default keywords self.cmds = read_config(default) # read in the users wishes if filename is not None: self.cmds.update(read_config(filename)) # option sim_data_dir overrides values in config files if sim_data_dir is not None: self.cmds['SIM_DATA_DIR'] = sim_data_dir # If we have no SIM_DATA_DIR from config or parameter, exit. if self.cmds['SIM_DATA_DIR'] == 'None' \ or self.cmds['SIM_DATA_DIR'] is None: raise ValueError("Please specify config file and/or sim_data_dir!") # add the instrument-specific data directory to the package # search path if sim.__search_path__[1] != self.cmds['SIM_DATA_DIR']: sim.__search_path__.insert(1, self.cmds['SIM_DATA_DIR']) # into python None values self._convert_none() self._find_files() self._default_data() self.cmds["CONFIG_USER"] = filename self.cmds["CONFIG_DEFAULT"] = default if self.cmds["SIM_PSF_OVERSAMPLE"] == "yes": self.cmds["PSF_MODE"] = "oversample" else: self.cmds["PSF_MODE"] = "linear_interp" # Subcategories of parameters, filled later by _split_categories # in _update_attributes self.obs = None self.sim = None self.atmo = None self.scope = None self.inst = None self.fpa = None self.hxrg = None # update the UserCommand "special" attributes self._update_attributes() if self.verbose and filename is not None: print("Read in parameters from " + filename) logging.debug("Read in parameters from " + filename)
[docs] def update(self, new_dict): """ Update multiple entries of a ``UserCommands`` dictionary ``update(new_dict)`` takes either a normal python ``dict`` object or a ``UserCommands`` object. Only keywords that match those in the ``UserCommands`` object will be updated. Misspelled keywords raise an error. To update single items in the dictionary, it is recommended to simply call the key and update the value - i.e ``<UserCommands>[key] = value``. Parameters ---------- new_dict : dict, ``UserCommands`` Raises ------ KeyError If a parameter is not found in ``self.cmds``. See Also -------- UserCommands Notes ----- Examples -------- View the default commands >>> import simcado >>> my_cmds = simcado.UserCommands() >>> print(my_cmds.cmds) Change a single command >>> my_cmds["OBS_DIT"] = 60 Change a series of commands at once >>> new_cmds = {"OBS_DIT" : 60 , "OBS_NDIT" : 10} >>> my_cmds.update(new_cmds) """ if isinstance(new_dict, UserCommands): tmpcmds = new_dict.cmds elif isinstance(new_dict, dict): tmpcmds = new_dict else: raise ValueError("Cannot update with type: "+type(new_dict)) for thekey in tmpcmds: if not thekey in self.cmds: raise KeyError("Unknown parameter " + thekey) else: self.cmds[thekey] = tmpcmds[thekey] self._find_files() self._default_data() self._update_attributes()
[docs] def keys(self): """ Return the keys in the `UserCommands.cmds` dictionary """ return self.cmds.keys()
[docs] def values(self): """ Return the values in the `UserCommands.cmds` dictionary """ return self.cmds.values()
[docs] def writeto(self, filename="commands.config"): """ Write all the key-value commands to an ASCII file on disk Parameters ---------- filename : str file path for where the file should be saved """ outstr = "" for group in (self.obs, self.sim, self.atmo, self.scope, self.inst, self.fpa, self.hxrg): for key in group: val = self[key] if key == "FPA_CHIP_LAYOUT" and "\n" in val: val = "small" outstr += key.ljust(25)+" "+str(val) + "\n" outstr += "\n" with open(filename, "w") as fd1: fd1.write(outstr)
def _convert_none(self): """ Turn all string "none" or "None" values into python ``None`` values """ for key in self.cmds: value = self.cmds[key] if isinstance(value, str) and value.lower() == "none": self.cmds[key] = None def _find_files(self): """ Checks for files in the package search path The search path comprises the directories: "./", SIM_DATA_DIR, <pkg_dir>, <pkg_dir>/data """ for key in self.cmds: if key == "OBS_OUTPUT_DIR": # need not exist continue keyval = self.cmds[key] # not a string: not a filename if not isinstance(keyval, str): continue # If string has no extension, assume it's not a file name. # This is a strong assumption, but we need to guard from # looking for "yes", "no", "none", "scao", etc. # TODO Can we have a list of reserved keywords? if "." in keyval and len(keyval.split(".")[-1]) > 1: # look for the file fname = find_file(keyval, silent=True) if fname is None: warnings.warn("Keyword "+key+" path doesn't exist: " + keyval) else: self.cmds[key] = fname def _default_data(self): """ Input system-specific path names for the default package data files """ if isinstance(self.cmds["SCOPE_PSF_FILE"], str): if self.cmds["SCOPE_PSF_FILE"].lower() in ["ltao"]: self.cmds["SCOPE_PSF_FILE"] = find_file("PSF_LTAO.fits") elif self.cmds["SCOPE_PSF_FILE"].lower() in ("default", "scao"): self.cmds["SCOPE_PSF_FILE"] = find_file("PSF_SCAO.fits") self.cmds["INST_USE_AO_MIRROR_BG"] = "no" elif self.cmds["SCOPE_PSF_FILE"].lower() in ("mcao", "maory"): print("Unfortunately SimCADO doesn't yet have a MCAO PSF") print("Using the SCAO PSF instead") self.cmds["SCOPE_PSF_FILE"] = find_file("PSF_SCAO.fits") elif self.cmds["SCOPE_PSF_FILE"].lower() in ("poppy", "ideal"): self.cmds["SCOPE_PSF_FILE"] = find_file("PSF_POPPY.fits") elif find_file(self.cmds["SCOPE_PSF_FILE"]) is None: raise ValueError("Cannot recognise PSF file name: " + self.cmds["SCOPE_PSF_FILE"]) elif isinstance(self.cmds["SCOPE_PSF_FILE"], PSFCube): pass elif self.cmds["SCOPE_PSF_FILE"] is None: msg = "SCOPE_PSF_FILE is None - Generating PSF from OBS_SEEING" warnings.warn(msg) logging.debug(msg) else: raise ValueError("Cannot recognise SCOPE_PSF_FILE: " + self.cmds["SCOPE_PSF_FILE"]) if self.cmds["INST_MIRROR_TC"] == "default": self.cmds["INST_MIRROR_TC"] = self.cmds["SCOPE_M1_TC"] if self.cmds["INST_MIRROR_AO_TC"] == "default": self.cmds["INST_MIRROR_AO_TC"] = self.cmds["INST_MIRROR_TC"] # which detector chip to use layout_dict = {"tiny" : "_tiny", "small" : "_small", "none": "_small", "centre" : "_centre", "center" : "_centre", "full" : "", "all" : ""} if self.cmds["FPA_CHIP_LAYOUT"] in layout_dict: layout = layout_dict[self.cmds["FPA_CHIP_LAYOUT"]] fname = "FPA_chip_layout" + layout + ".dat" self.cmds["FPA_CHIP_LAYOUT"] = fname self.cmds["FPA_CHIP_LAYOUT"] = find_file(self.cmds["FPA_CHIP_LAYOUT"]) def _update_attributes(self): """ Update the UserCommand convenience attributes """ if self.cmds["SCOPE_MIRROR_LIST"] is not None: self.mirrors_telescope = ioascii.read(self.cmds["SCOPE_MIRROR_LIST"]) else: raise ValueError("SCOPE_MIRROR_LIST = " + \ self.cmds["SCOPE_MIRROR_LIST"]) if self.cmds["INST_MIRROR_AO_LIST"] is not None: self.mirrors_ao = ioascii.read(self.cmds["INST_MIRROR_AO_LIST"]) else: self.mirrors_ao = ioascii.read(""" #Mirror Outer Inner Temp M0 0. 0. -273 """) i = np.where(self.mirrors_telescope["Mirror"] == "M1")[0][0] self.diameter = self.mirrors_telescope["Outer"][i] self.area = np.pi / 4 * (self.diameter**2 - self.mirrors_telescope["Inner"][i]**2) # Check for a filter curve file or a standard broadband name if isinstance(self.cmds["INST_FILTER_TC"], str): fname = find_file(self.cmds["INST_FILTER_TC"]) if fname is None: # try the name of the filter tryname = "TC_filter_" + self.cmds["INST_FILTER_TC"] + ".dat" fname = find_file(tryname) if fname is None: raise ValueError("Filter " + self.cmds["INST_FILTER_TC"] + "could not be found") self.cmds["INST_FILTER_TC"] = fname self.fpa_res = self.cmds["SIM_DETECTOR_PIX_SCALE"] self.pix_res = self.fpa_res / self.cmds["SIM_OVERSAMPLING"] # If OBS_ZENITH_DIST is specified it overrides ATMO_AIRMASS, if self.cmds["OBS_ZENITH_DIST"] is not None: self.cmds["ATMO_AIRMASS"] = \ sim.utils.zendist2airmass(self.cmds["OBS_ZENITH_DIST"]) # if SIM_USE_FILTER_LAM is true, then use the filter curve to set the # wavelength boundaries where the filter is < SIM_FILTER_THRESHOLD if self.cmds["SIM_USE_FILTER_LAM"].lower() == "yes": if isinstance(self.cmds["INST_FILTER_TC"], str): tc_filt = sc.TransmissionCurve(filename=self.cmds['INST_FILTER_TC']) else: tc_filt = self.cmds["INST_FILTER_TC"] mask = np.where(tc_filt.val > self.cmds["SIM_FILTER_THRESHOLD"])[0] imin = np.max((mask[0] - 1, 0)) imax = np.min((mask[-1] + 1, len(tc_filt.lam) - 1)) lam_min, lam_max = tc_filt.lam[imin], tc_filt.lam[imax] else: lam_min = self.cmds["SIM_LAM_MIN"] lam_max = self.cmds["SIM_LAM_MAX"] self.lam_res = self.cmds["SIM_LAM_TC_BIN_WIDTH"] self.lam = np.arange(lam_min, lam_max + 1E-7, self.lam_res) # self.lam_psf_res = self.cmds["SIM_LAM_PSF_BIN_WIDTH"] # self.lam_bin_edges = np.arange(lam_min, # lam_max + self.lam_psf_res + 1E-7, # self.lam_psf_res) # make lam_bin_edges according to how great the ADC offsets are self.lam_bin_edges = self._get_lam_bin_edges(lam_min, lam_max) self.lam_bin_centers = 0.5 * (self.lam_bin_edges[1:] + \ self.lam_bin_edges[:-1]) # total integration time. TODO necessary? self.exptime = self.cmds['OBS_DIT'] * self.cmds['OBS_NDIT'] # replace 'none', 'None' with None self._convert_none() self.verbose = (self.cmds["SIM_VERBOSE"] == "yes") self._get_total_wfe() self._split_categories() def _get_total_wfe(self): """ Gets the total wave front error from the table in INST_WFE_LIST """ if self.cmds["INST_WFE"] is not None: if isinstance(self.cmds["INST_WFE"], str): wfe_list = ioascii.read(self.cmds["INST_WFE"]) wfe = wfe_list[wfe_list.colnames[0]] num = wfe_list[wfe_list.colnames[1]] elif isinstance(self.cmds["INST_WFE"], (float, int)): wfe, num = float(self.cmds["INST_WFE"]), 1 tot_wfe = np.sqrt(np.sum(num * wfe**2)) else: tot_wfe = 0 self.cmds["INST_TOTAL_WFE"] = tot_wfe def _get_lam_bin_edges(self, lam_min, lam_max): """ Generates an array with the bin edges of the layers in spectral space Parameters ---------- lam_min, lam_max : float [um] the minimum and maximum wavelengths of the filter range Notes ------- Atmospheric diffraction causes blurring in an image. To model this effect the spectra from a ``Source`` object are cut into bins based on how far the photons at different wavelength are diffracted from the image center. The threshold for defining a new layer based on the how far a certain bin will move is given by ``SIM_ADC_SHIFT_THRESHOLD``. The default value is 1 pixel. The PSF also causes blurring as it spreads out over a bandpass. This also needed to be taken into account """ if self.cmds["SIM_VERBOSE"] == "yes": print("Determining lam_bin_edges") logging.debug("[UserCommands] Determining lam_bin_edges") effectiveness = self.cmds["INST_ADC_PERFORMANCE"] / 100. # This is redundant because also need to look at the PSF width # if effectiveness == 1.: # lam_bin_edges = np.array([lam_min, lam_max]) # return lam_bin_edges shift_threshold = self.cmds["SIM_ADC_SHIFT_THRESHOLD"] # get the angle shift for each slice lam = np.arange(lam_min, lam_max + 1E-7, 0.001) zenith_distance = sim.utils.airmass2zendist(self.cmds["ATMO_AIRMASS"]) angle_shift = atmospheric_refraction(lam, zenith_distance, self.cmds["ATMO_TEMPERATURE"], self.cmds["ATMO_REL_HUMIDITY"], self.cmds["ATMO_PRESSURE"], self.cmds["SCOPE_LATITUDE"], self.cmds["SCOPE_ALTITUDE"]) # convert angle shift into number of pixels # pixel shifts are defined with respect to last slice rel_shift = (angle_shift - angle_shift[-1]) / self.pix_res rel_shift *= (1. - effectiveness) if np.max(np.abs(rel_shift)) > 1000: raise ValueError("Pixel shifts too great (>1000), check units") # Rotate by the paralytic angle int_shift = np.array(rel_shift / shift_threshold, dtype=np.int) idx = [np.where(int_shift == i)[0][0] for i in np.unique(int_shift)[::-1]] lam_bin_edges_adc = np.array(lam[idx + [len(lam)-1]]) # Now check to see if the PSF blurring is the controlling factor. If so, # take the lam_bin_edges for the PSF blurring diam = self.diameter d_ang = self.pix_res * shift_threshold lam_bin_edges_psf = [lam_min] ang0 = (lam_min*1E-6) / diam * 1.22*53*3600 i = 1 while lam_bin_edges_psf[-1] < lam_max: lam_bin_edges_psf += [(ang0 + d_ang*i) * diam / (1.22*53*3600) * 1E6] i += 1 if i > 1000: raise ValueError("lam_bin_edges needs >1000 values") lam_bin_edges_psf[-1] = lam_max lam_bin_edges = np.unique(np.concatenate( (np.round(lam_bin_edges_psf, 3), np.round(lam_bin_edges_adc, 3)))) if self.cmds["SIM_VERBOSE"] == "yes": print("PSF edges were", np.round(lam_bin_edges_psf, 3)) print("ADC edges were", np.round(lam_bin_edges_adc, 3)) print("All edges were", np.round(lam_bin_edges, 3)) logging.debug("[UserCommands] lam_bin_edges found") return lam_bin_edges def _split_categories(self): """ Generate smaller category-specific dictionaries """ self.obs = {i:self.cmds[i] for i in self.cmds.keys() if "OBS" in i} self.sim = {i:self.cmds[i] for i in self.cmds.keys() if "SIM" in i} self.atmo = {i:self.cmds[i] for i in self.cmds.keys() if "ATMO" in i} self.scope = {i:self.cmds[i] for i in self.cmds.keys() if "SCOPE" in i} self.inst = {i:self.cmds[i] for i in self.cmds.keys() if "INST" in i} self.fpa = {i:self.cmds[i] for i in self.cmds.keys() if "FPA" in i} self.hxrg = {i:self.cmds[i] for i in self.cmds.keys() if "HXRG" in i} def __str__(self): if self.cmds["CONFIG_USER"] is not None: return "A dictionary of commands compiled from " + \ self.cmds["CONFIG_USER"] return "A dictionary of default commands" def __iter__(self): return self.cmds.__iter__() def __getitem__(self, key): return self.cmds[key] def __setitem__(self, key, val): if key not in self.cmds.keys(): raise ValueError(key+" not in UserCommands.keys()") if isinstance(val, str) and key == "INST_FILTER_TC" \ and "TC_filter" not in val: val = "TC_filter_{}.dat".format(val) self.cmds[key] = val self._find_files() self._default_data() self._update_attributes()
# Add to the update that all the cmds.variable are updated when # the dicts are updated
[docs]def dump_defaults(filename=None, selection="freq"): # OC, 2016-08-11: changed parameter from 'type' to 'selection' as # 'type' redefines built-in """ Dump the frequent.config file to a path specified by the user Parameters ---------- filename : str, optional path or filename where the .config file is to be saved selection : str, optional ["freq", "all"] amount of keywords to save. "freq" only prints the most frequently used keywords. "all" prints all of them """ if "freq" in selection.lower(): fname = "frequent.config" elif "all" in selection.lower(): fname = "default.config" if filename is None: gname = os.path.join(__pkg_dir__, "data", fname) with open(gname, "r") as fd1: print(fd1.read()) return None else: path, gname = os.path.split(filename) if path == "": path = "." if gname == "": gname = fname shutil.copy(os.path.join(__pkg_dir__, "data", fname), os.path.join(path, gname))
[docs]def dump_chip_layout(path=None): # OC, 2016-08-11: changed parameter from 'dir' (redefines built-in) """ Dump the FPA_chip_layout.dat file to a path specified by the user Parameters ---------- path : str, optional path where the chip layout file is to be saved """ fname = find_file("FPA_chip_layout.dat") if path is None: with open(fname, "r") as fd1: print(fd1.read()) else: path = os.path.dirname(path) shutil.copy(fname, path) logging.debug("Printed chip layout to file: "+path+"/"+fname)
[docs]def dump_mirror_config(path=None, what="scope"): """ Dump the EC_mirrors_scope.tbl or the EC_mirrors_ao.tbl to disk Parameters ---------- path : str, optional path where the mirror configuration file is to be saved what : str, optional ["scope", "ao"] dump the mirror configuration for either the telescope or the AO module """ if what.lower() == "scope": print("Dumping telescope mirror configuration.") fname = find_file("EC_mirrors_scope.tbl") elif what.lower() == "ao": print("Dumping AO mirror configuration.") fname = find_file("EC_mirrors_ao.tbl") if path is None: with open(fname, "r") as fd1: print(fd1.read()) else: path = os.path.dirname(path) shutil.copy(fname, path)
[docs]def read_config(config_file): """ Read in a SimCADO configuration file The configuration file is in SExtractor format: 'PARAMETER Value # Comment' Parameters ---------- config_file : str the filename of the .config file Returns ------- config_dict : dict (collections.OrderedDict) A dictionary with keys 'PARAMETER' and values 'Value'. Notes ----- The values of the dictionary are strings and will have to be converted to the appropriate data type as they are needed. """ import re # Read the file into a list of strings config_dict = OrderedDict() # remove lines that are all spaces or spaces + '#' # these are the regular expressions isempty = re.compile(r'^\s*$') iscomment = re.compile(r'^\s*#') with open(config_file, 'r') as fp1: for line in fp1: if isempty.match(line): continue if iscomment.match(line): continue line = line.rstrip() # remove trailing \n content = line.split('#', 1)[0] # remove comment param, value = content.split(None, 1) # Convert to number if possible try: config_dict[param] = float(value.strip()) except ValueError: config_dict[param] = value.strip() # Convert string "none" to python None if isinstance(value, str) and value.lower() == "none": config_dict[param] = None return config_dict
[docs]def update_config(config_file, config_dict): """ Update a SimCADO configuration dictionary A configuration file in the SExtractor format: :: 'PARAMETER Value # Comment' an existing configuration dictionary. Parameters ---------- config_file : str the filename of the .config file Returns ------- config_dict : dict A dictionary with keys 'PARAMETER' and values 'Value'. Notes ----- the values of the dictionary are strings and will have to be converted to the appropriate data type as they are needed. """ config_dict.update(read_config(config_file)) return config_dict