#!/usr/bin/python
# params.py

'''
Classes and functions for creating and reading parameters files
'''

#geonomics imports
from geonomics.utils._str_repr_ import _get_str_spacing

#other imports
import os, time, datetime
import numpy as np
import pandas as pd
import copy
import re


######################################
# -----------------------------------#
# VARIABLES -------------------------#
# -----------------------------------#
######################################

    ##################
    # params strings #
    ##################

#main text-block
#STRING SLOTS:
    #%s = layers_params,
    #%s = spps_params,
    #%s = its_params,
    #%s = data_params,
    #%s = stats_params,
PARAMS = '''# %s

# This is a parameters file generated by Geonomics
# (by the gnx.make_parameters_file() function).


                   #   ::::::          :::    :: :::::::::::#
             #::::::    ::::   :::      ::    :: :: ::::::::::: ::#
          #:::::::::     ::            ::   :::::::::::::::::::::::::#
        #::::::::::                      :::::::::: :::::: ::::::::  ::#
      #  : ::::  ::                    ::::  : ::    :::::::: : ::  :    #
     # GGGGG :EEEE: OOOOO   NN   NN   OOOOO   MM   MM IIIIII  CCCCC SSSSS #
    # GG     EE    OO   OO  NNN  NN  OO   OO  MM   MM   II   CC     SS     #
    # GG     EE   OO     OO NN N NN OO     OO MMM MMM   II   CC     SSSSSS #
    # GG GGG EEEE OO     OO NN  NNN OO     OO MM M MM   II   CC         SS #
    # GG   G EE    OO   OO  NN   NN  OO   OO  MM   MM   II   CC        SSS #
     # GGGGG :EEEE: OOOOO   NN   NN   OOOOO   MM   MM IIIIII  CCCCC SSSSS #
      #    : ::::::::               :::::::::: ::              ::  :   : #
        #:    :::::                    :::::: :::             :::::::  #
          #    :::                      :::::  ::              ::::: #
             #  ::                      ::::                      #
                   #                                        #
                      #  :: ::    :::             #


params = {
#-----------------------------------------------------------------------------#

#-----------------#
#--- LANDSCAPE ---#
#-----------------#
    'landscape': {

    #------------#
    #--- main ---#
    #------------#
        'main': {
            #x,y (a.k.a. j,i) dimensions of the Landscape
            'dim':                      (20,20),
            #x,y resolution of the Landscape
            'res':                      (1,1),
            #x,y coords of upper-left corner of the Landscape
            'ulc':                      (0,0),
            #projection of the Landscape
            'prj':                      None,
            }, # <END> 'main'

    #--------------#
    #--- layers ---#
    #--------------#
        'layers': {
%s


    #### NOTE: Individual Layers' sections can be copy-and-pasted (and
    #### assigned distinct keys and names), to create additional Layers.


            } # <END> 'layers'

        }, # <END> 'landscape'


#-----------------------------------------------------------------------------#

#-----------------#
#--- COMMUNITY ---#
#-----------------#
    'comm': {

        'species': {
%s


    #### NOTE: individual Species' sections can be copy-and-pasted (and
    #### assigned distinct keys and names), to create additional Species.


            }, # <END> 'species'

        }, # <END> 'comm'


#-----------------------------------------------------------------------------#

#-------------#
#--- MODEL ---#
#-------------#
    'model': {
        #total Model runtime (in timesteps)
        'T':            100,
        #min burn-in runtime (in timesteps)
        'burn_T':       30,
        #seed number
        'num':          None,

%s
%s
%s
        } # <END> 'model'

    } # <END> params
'''

#block for lyr params
#STRING SLOTS: 
    #%s = lyr_name,
    #%i = lyr_num,
    #%s = lyr_type_params,
    #%s = lyr_change_params,
    #%i = lyr_num,
LYR_PARAMS = '''
            #layer name (LAYER NAMES MUST BE UNIQUE!)
            %s: {

        #-------------------------------------#
        #--- layer num. %i: init parameters ---#
        #-------------------------------------#

                #initiating parameters for this layer
                'init': {
%s
                    }, # <END> 'init'
%s
                }, # <END> layer num. %i
'''

#the block of random-layer parameters
RAND_LYR_PARAMS = '''
                    #parameters for a 'random'-type Layer
                    'random': {
                        #number of random points
                        'n_pts':                        500,
                        #interpolation method {'linear', 'cubic', 'nearest'}
                        'interp_method':                'linear',

                        }, # <END> 'random'
'''

#the block of defined-layer parameters
DEFINED_LYR_PARAMS = '''
                    #parameters for a 'defined'-type Layer
                    'defined': {
                        #raster to use for the Layer
                        'rast':                   np.ones((20,20)),
                        #point coordinates
                        'pts':                    None,
                        #point values
                        'vals':                   None,
                        #interpolation method {None, 'linear', 'cubic',
                        #'nearest'}
                        'interp_method':          None,

                        }, # <END> 'defined'
'''

#the block of file-layer parameters
FILE_LYR_PARAMS = '''
                    #parameters for a 'file'-type Layer
                    'file': {
                        #</path/to/file>.<ext>
                        'filepath':                     '/PATH/TO/FILE.EXT',
                        #minimum value to use to rescale the Layer to [0,1]
                        'scale_min_val':                None,
                        #maximum value to use to rescale the Layer to [0,1]
                        'scale_max_val':                None,
                        #decimal precision to use for coord-units (ulc & res)
                        'coord_prec':                   5,
                        #units of this file's variable
                        'units':                        None,

                        }, # <END> 'file'
'''

#the block of nlmpy-layer parameters
NLMPY_LYR_PARAMS = '''
                    #parameters for an 'nlmpy'-type Layer
                    'nlmpy': {
                        #nlmpy function to use the create this Layer
                        'function':                 'mpd',
                        #number of rows (MUST EQUAL LANDSCAPE DIMENSION y!)
                        'nRow':                     20,
                        #number of cols (MUST EQUAL LANDSCAPE DIMENSION x!)
                        'nCol':                     20,
                        #level of spatial autocorrelation in element values
                        'h':                        1,

                        }, # <END> 'nlmpy'
'''

#block of layer-change parameters
#STRING SLOTS:
    #%i = lyr_num,
    #%s = change events,
LYR_CHANGE_PARAMS = '''
            #---------------------------------------#
            #--- layer num. %i: change parameters ---#
            #---------------------------------------#

                #landscape-change events for this Layer
                'change': {
%s

                    }, # <END> 'change'
'''

#block for params in a single layer-change event
#STRING SLOTS:
    #%i = lyr_change_event_num,
    #%i = lyr_change_event_num,
LYR_CHANGE_EVENT_PARAMS = '''
                    %i: {
                        #array or file for final raster of event, or directory
                        #of files for each stepwise change in event
                        'change_rast':      '/PATH/TO/FILE.EXT',
                        #starting timestep of event
                        'start_t':          49,
                        #ending timestep of event
                        'end_t':            99,
                        #number of stepwise changes in event
                        'n_steps':          5,
                        }, # <END> event %i'''

#block of species params
#STRING SLOTS:
    #%s = spp_name,
    #%i = spp_num,
    #%s = msprime_init_params,
    #%i = spp_num,
    #%i = spp_num,
    #%i = spp_num,
    #%s = move_surf_params,
    #%s = disp_surf_params,
    #%s = genome_params,
    #%s = change_params,
    #%i = spp_num,
SPP_PARAMS = '''
            #species name (SPECIES NAMES MUST BE UNIQUE!)
            %s: {

            #-----------------------------------#
            #--- spp num. %i: init parameters ---#
            #-----------------------------------#

                'init': {
                    #starting number of individs
                    'N':                250,
                    #carrying-capacity Layer name
                    'K_layer':          'lyr_0',
                    #multiplicative factor for carrying-capacity layer
                    'K_factor':         1,%s
                    }, # <END> 'init'

            #-------------------------------------#
            #--- spp num. %i: mating parameters ---#
            #-------------------------------------#

                'mating'    : {
                    #age(s) at sexual maturity (if tuple, female first)
                    'repro_age':                0,
                    #whether to assign sexes
                    'sex':                      False,
                    #ratio of males to females
                    'sex_ratio':                1/1,
                    #intrinsic growth rate
                    'R':                        0.5,
                    #intrinsic birth rate (MUST BE 0<=b<=1)
                    'b':                        0.2,
                    #expectation of distr of n offspring per mating pair
                    'n_births_distr_lambda':    1,
                    #whether n births should be fixed at n_births_dist_lambda
                    'n_births_fixed':           True,
                    #radius of mate-search area (None, for panmixia)
                    'mating_radius':            10,
                    #whether individs should choose nearest neighs as mates
                    'choose_nearest_mate':        False,
                    #whether mate-choice should be inverse distance-weighted
                    'inverse_dist_mating':      False,
                    }, # <END> 'mating'

            #----------------------------------------#
            #--- spp num. %i: mortality parameters ---#
            #----------------------------------------#

                'mortality'     : {
                    #maximum age
                    'max_age':                      None,
                    #min P(death) (MUST BE 0<=d_min<=1)
                    'd_min':                        0,
                    #max P(death) (MUST BE 0<=d_max<=1)
                    'd_max':                        1,
                    #width of window used to estimate local pop density
                    'density_grid_window_width':    None,
                    }, # <END> 'mortality'

            #---------------------------------------#
            #--- spp num. %i: movement parameters ---#
            #---------------------------------------#

                'movement': {
                    #whether or not the species is mobile
                    'move':                                 True,
                    #mode of distr of movement direction
                    'direction_distr_mu':                   0,
                    #concentration of distr of movement direction
                    'direction_distr_kappa':                0,
                    #1st param of distr of movement distance
                    'movement_distance_distr_param1':       0.01,
                    #2nd param of distr of movement distance
                    'movement_distance_distr_param2':       0.5,
                    #movement distance distr to use ('lognormal','levy','wald')
                    'movement_distance_distr':              'lognormal',
                    #1st param of distr of dispersal distance
                    'dispersal_distance_distr_param1':      -1,
                    #2nd param of distr of dispersal distance
                    'dispersal_distance_distr_param2':      0.05,
                    #dispersal distance distr to use ('lognormal','levy','wald')
                    'dispersal_distance_distr':             'lognormal',%s%s
                    },    # <END> 'movement'

%s
%s
                }, # <END> spp num. %i
'''

# block for params to initiate model with
# individuals drawn from msprime simulations
MSPRIME_INIT_PARAMS = '''
                    # params for 1+ msprime pops for sampling starting individs
                    'msprime':        {%s
                        },
'''

MSPRIME_POP_PARAMS = '''
                        # index of msprime source pop
                        %i:  {
                            # number of individs to sample from pop
                            100: {
                                 # 1x2 coord pair or nx2 coord pairs of individs
                                 'coords':              [0, 0],
                                 # valid kwargs dict for Model.add_individuals
                                 # 'source_msprime_params' argument...
                                 'recomb_rate':         0.5,
                                 'mut_rate':            0.0001,
                                 'demography':          None,
                                 'population_size':     None,
                                 'ancestry_model':      None,
                                 'random_seed':         None,
                                 }
                            },

'''

#block for movement-surface params
MOVE_SURF_PARAMS = '''
                    'move_surf'     : {
                        #move-surf Layer name
                        'layer':                'lyr_0',
                        #whether to use mixture distrs
                        'mixture':              True,
                        #concentration of distrs
                        'vm_distr_kappa':       12,
                        #length of approximation vectors for distrs
                        'approx_len':           5000,
                        }, # <END> 'move_surf'
'''

#block for dispersal-surface params
DISP_SURF_PARAMS = '''
                    'disp_surf'     : {
                        #disp-surf Layer name
                        'layer':                'lyr_0',
                        #whether to use mixture distrs
                        'mixture':              True,
                        #concentration of distrs
                        'vm_distr_kappa':       12,
                        #length of approximation vectors for distrs
                        'approx_len':           5000,
                        }, # <END> 'disp_surf'
'''

#block for genome params
#STRING SLOTS:
    #%i = spp_num,
    #
    #%s = traits_params,
GENOME_PARAMS = '''
            #---------------------------------------------------#
            #--- spp num. %i: genomic architecture parameters ---#
            #---------------------------------------------------#

                'gen_arch': {
                    #file defining custom genomic arch
                    'gen_arch_file':            %s,
                    #num of loci
                    'L':                        100,
                    #fixed starting allele freq; None/False -> rand; True -> 0.5
                    'start_p_fixed':            0.5,
                    #whether to start neutral locus freqs at 0
                    'start_neut_zero':          False,
                    #genome-wide per-base neutral mut rate (0 to disable)
                    'mu_neut':                  0,
                    #genome-wide per-base deleterious mut rate (0 to disable)
                    'mu_delet':                 0,
                    #shape of distr of deleterious effect sizes
                    'delet_alpha_distr_shape':  0.2,
                    #scale of distr of deleterious effect sizes
                    'delet_alpha_distr_scale':  0.2,
                    #alpha of distr of recomb rates
                    'r_distr_alpha':            0.5,
                    #beta of distr of recomb rates
                    'r_distr_beta':             None,
                    #whether loci should be dominant (for allele '1')
                    'dom':                      False,
                    #whether to allow pleiotropy
                    'pleiotropy':               False,
                    #custom fn for drawing recomb rates
                    'recomb_rate_custom_fn':    None,
                    #number of recomb paths to hold in memory
                    'n_recomb_paths_mem':       int(1e4),
                    #total number of recomb paths to simulate
                    'n_recomb_paths_tot':       int(1e5),
                    #num of crossing-over events (i.e. recombs) to simulate
                    'n_recomb_sims':            10_000,
                    #whether to generate recombination paths at each timestep
                    'allow_ad_hoc_recomb':      False,
                    #whether to jitter recomb bps, to correctly track num_trees
                    'jitter_breakpoints':       False,
                    #whether to save mutation logs
                    'mut_log':                  False,
                    #whether to use tskit (to record full spatial pedigree)
                    'use_tskit':                True,
                    #time step interval for simplication of tskit tables
                    'tskit_simp_interval':      100,

%s
                    }, # <END> 'gen_arch'
'''

#block for traits params
#STRING SLOTS:
    #%s = multi_trait_params,
TRTS_PARAMS = '''
                    'traits': {
%s

    #### NOTE: Individual Traits' sections can be copy-and-pasted (and
    #### assigned distinct keys and names), to create additional Traits.


                        }, # <END> 'traits'
'''

#block for trait params
#STRING SLOTS:
    #%i = trait_num,
    #%s = trait_num,
    #%i = trait_num,
TRT_PARAMS = '''
                        #-------------------------#
                        #---trait %i parameters ---#
                        #-------------------------#
                        #trait name (TRAIT NAMES MUST BE UNIQUE!)
                        %s: {
                            #trait-selection Layer name
                            'layer':                'lyr_0',
                            #phenotypic selection coefficient
                            'phi':                  0.05,
                            #number of loci underlying trait
                            'n_loci':               1,
                            #mutation rate at loci underlying trait
                            'mu':                   0,
                            #mean of distr of effect sizes
                            'alpha_distr_mu' :      0.1,
                            #variance of distr of effect size
                            'alpha_distr_sigma':    0,
                            #max allowed magnitude for an alpha value
                            'max_alpha_mag':        None,
                            #curvature of fitness function
                            'gamma':                1,
                            #whether the trait is universally advantageous
                            'univ_adv':             False
                            }, # <END> trait %i
'''

#block of spp_change params
#STRING SLOTS:
    #%i = spp_num,
    #%s = dem_and-or_param_change_params_str,
SPP_CHANGE_PARAMS = '''
            #-------------------------------------#
            #--- spp num. %i: change parameters ---#
            #-------------------------------------#

                'change': {
%s
                        } # <END> 'change'
'''

#block for a series of demographic-change events
#STRING SLOTS:
    #%s = multi_dem_change_event_params,
SPP_DEM_CHANGE_EVENTS_PARAMS = '''
                    #-------------------------------------#
                    #--- demographic change parameters ---#
                    #-------------------------------------#
                    'dem': {
%s


    #### NOTE: Individual demographic change events' sections can be
    #### copy-and-pasted (and assigned distinct keys and names), to create
    #### additional events.


                        }, # <END> 'dem'
'''

#block for parameters for a single demographic-change event
#STRING SLOTS:
    #%i = dem_change_event_num,
    #%i = dem_change_event_num,
SPP_DEM_CHANGE_EVENT_PARAMS = '''
                        %i: {
                            #kind of event {'monotonic', 'stochastic',
                            #'cyclical', 'custom'}
                            'kind':             'monotonic',
                            #starting timestep
                            'start_t':          49,
                            #ending timestep
                            'end_t':            99,
                            #rate, for monotonic change
                            'rate':             1.02,
                            #interval of changes, for stochastic change
                            'interval':         1,
                            #distr, for stochastic change {'uniform', 'normal'}
                            'distr':            'uniform',
                            #num cycles, for cyclical change
                            'n_cycles':         10,
                            #min & max sizes, for stochastic & cyclical change
                            'size_range':       (0.5, 1.5),
                            #list of timesteps, for custom change
                            'timesteps':        [50, 90, 95],
                            #list of sizes, for custom change
                            'sizes':            [2, 5, 0.5],
                            }, # <END> event %i

'''

#block for a series of life-history parameter-change events
SPP_PARAM_CHANGE_PARAMS = '''
                    #--------------------------------------#
                    #--- life-history change parameters ---#
                    #--------------------------------------#
                    'life_hist': {
                        #life-history parameter to change
                        '<life_hist_param>': {
                            #list of timesteps
                            'timesteps':        [],
                            #list of values
                            'vals':             [],
                                }


    #### NOTE: Individual life-history paramter change events' sections can be
    #### copy-and-pasted (and assigned distinct keys and names), to create
    #### additional events.


                            }, # <END> 'life_hist'
'''

#block for model iterations params
ITS_PARAMS = '''
        #-----------------------------#
        #--- iterations parameters ---#
        #-----------------------------#
        'its': {
            #num iterations
            'n_its':            1,
            #whether to randomize Landscape each iteration
            'rand_landscape':   False,
            #whether to randomize Community each iteration
            'rand_comm':        False,
            #whether to randomize GenomicArchitectures each iteration
            'rand_genarch':     True,
            #whether to burn in each iteration
            'repeat_burn':      False,
            }, # <END> 'iterations'
'''

#block for model data-collection params
DATA_PARAMS = '''
        #----------------------------------#
        #--- data-collection parameters ---#
        #----------------------------------#
        'data': {
            'sampling': {
                #sampling scheme {'all', 'random', 'point', 'transect'}
                'scheme':               'random',
                #sample size at each point, for point & transect sampling
                'n':                    250,
                #coords of collection points, for point sampling
                'points':               None,
                #coords of transect endpoints, for transect sampling
                'transect_endpoints':   None,
                #num points along transect, for transect sampling
                'n_transect_points':    None,
                #collection radius around points, for point & transect sampling
                'radius':               None,
                #when to collect data
                'when':                 None,
                #whether to save current Layers when data is collected
                'include_landscape':    False,
                #whether to include fixed loci in VCF files
                'include_fixed_sites':  False,
                },
            'format': {
                #format for genetic data {'vcf', 'fasta'}
                'gen_format':           ['vcf', 'fasta'],
                #format for vector geodata {'csv', 'shapefile', 'geojson'}
                'geo_vect_format':      'csv',
                #format for raster geodata {'geotiff', 'txt'}
                'geo_rast_format':      'geotiff',
                #format for file listing non-neut loci (if 'csv', will be saved)
                'nonneut_loc_format': None,
                },
            }, #<END> 'data'
'''

#block for model stats-calculation params
STATS_PARAMS = '''
        #-----------------------------------#
        #--- stats-collection parameters ---#
        #-----------------------------------#
        'stats': {
            #number of individs at time t
            'Nt': {
                #whether to calculate
                'calc':     True,
                #calculation frequency (in timesteps)
                'freq':     1,
                },
            #heterozgosity
            'het': {
                #whether to calculate
                'calc':     True,
                #calculation frequency (in timesteps)
                'freq':     5,
                #whether to mean across sampled individs
                'mean': False,
                },
            #minor allele freq
            'maf': {
                #whether to calculate
                'calc':     True,
                #calculation frequency (in timesteps)
                'freq':     5,
                },
            #mean fitness
            'mean_fit': {
                #whether to calculate
                'calc':     True,
                #calculation frequency (in timesteps)
                'freq':     5,
                },
            #linkage disequilibirum
            'ld': {
                #whether to calculate
                'calc':     False,
                #calculation frequency (in timesteps)
                'freq':     100,
                },
            }, # <END> 'stats'
'''

######################################
# -----------------------------------#
# CLASSES ---------------------------#
# -----------------------------------#
######################################

#a _DynAttrDict dict class with k:v pairs as dynamic attributes
class _DynAttrDict(dict):
    def __getattr__(self, item):
        return self[item]
    def __dir__(self):
        return super().__dir__() + [str(k) for k in self.keys()]
    def __deepcopy__(self, memo):
        return _DynAttrDict(copy.deepcopy(dict(self)))

#a ParametersDict class (which is just a recursion the _DynAttrDict over the
#whole params dict, to make all its levels dicts with dynamic attributes, 
#i.e indexable by dot notation and responsive to tab completion)
class ParametersDict(_DynAttrDict):
    def __init__(self, params):
        params_dict = _make_params_dict(params)
        self.update(params)

    #re-enable deepcopy, because the class inherits from a dict
    def __deepcopy__(self, memo):
        return ParametersDict(copy.deepcopy(dict(self)))

    #define the __str__ and __repr__ special methods
    def __str__(self):
        #get a string representation of the class
        type_str = str(type(self))
        #get the model name str
        name_str = "Model name:%s%s"
        name_str = name_str % (_get_str_spacing(name_str), self.model.name)
        #concatenate the strings
        tot_str = '\n'.join([type_str, name_str])
        return tot_str

    def __repr__(self):
       repr_str = self.__str__()
       return repr_str


######################################
# -----------------------------------#
# FUNCTIONS -------------------------#
# -----------------------------------#
######################################

#function to create the lyrs-params section of a params file
def _make_lyrs_params_str(lyrs=1):
    #create an empty list, to be filled with one params string per lyr
    lyrs_params_list = []
    #if lyrs is an integer, create a string of identical parameter sections
    if type(lyrs) is int:
        #assert that it's an integer greater than 0
        assert lyrs > 0, ("The number of Layers to be created must be a "
        "positive integer.")
        #for each lyr
        for i in range(lyrs):
            #use lyr-type 'random'
            type_params = RAND_LYR_PARAMS
            #add no change params (i.e. a zero-length string)
            change_params = ''
            #create the lyr_params str
            lyr_params_str = LYR_PARAMS % ("'lyr_%i'" % i, i,
                                               type_params, change_params, i)
            #append it to the list
            lyrs_params_list.append(lyr_params_str)

    #or if lyrs is a list of dicts, then create individually customized
    #params sections for each Layer
    elif type(lyrs) is list:
        #assert that each item in the list is a dict
        assert False not in [type(item) is dict for item in lyrs], ("All "
            "items in the argument 'layers' must be of type dict if it is "
            "provided as a list.")
        assert False not in [type(item) is dict for item in lyrs], ("If the "
            "'layers' argument is a list then it must contain only "
            "objects of type dict.")
        #create lookup dicts for the params strings for different lyr params
        #sections
        lyr_type_params_str_dict = {'random': RAND_LYR_PARAMS,
                                      'defined': DEFINED_LYR_PARAMS,
                                      'file': FILE_LYR_PARAMS,
                                      'nlmpy': NLMPY_LYR_PARAMS,
                                     }
        #for each lyr
        for i, lyr_dict in enumerate(lyrs):
            #assert that the 'type' value is valid
            if 'type' in [*lyr_dict]:
                assert lyr_dict['type'] in ['random', 'defined', 'file',
                    'nlmpy'], ("The value provided for the 'type' of Layer "
                    "%i is invalid. Valid values include: ['random', "
                    "'defined', 'file', 'nlmpy'].") % i
                #get the type params for this lyr
                lyr_type = lyr_dict['type']
            else:
                lyr_type = 'random'
            type_params = lyr_type_params_str_dict[lyr_type]
            #assert that the 'change' value is valid
            if 'change' in [*lyr_dict]:
                assert (isinstance(lyr_dict['change'], bool)
                        or (isinstance(lyr_dict['change'], int)
                           and lyr_dict['change'] > 0)), ("The value "
                    "provided for the 'change' value of Layer %i is invalid. "
                    "Value must be either a boolean (to create a single "
                    "change event), or a positive int (to create a series "
                    "of that many change events).") % i
                #get the change params for this lyr
                lyr_change = lyr_dict['change']
            else:
                lyr_change = False
            #get the LYR_CHANGE_PARAMS, if change argument is True or int > 0
            if lyr_change is not False:
                change_params = LYR_CHANGE_PARAMS
                #get the necessary number of change events' params strings
                events_params = '\n'.join([LYR_CHANGE_EVENT_PARAMS % (n,
                    n) for n in range(lyr_change)])
            else:
                change_params = ''
            if change_params != '':
                change_params = change_params % (i, events_params)
            #create the lyr_params str for this Layer
            lyr_params_str = LYR_PARAMS % ("'lyr_%i'" % i, i,
                                               type_params, change_params, i)
            #append it to the list
            lyrs_params_list.append(lyr_params_str)

    #join the whole list into one str
    lyrs_params_str = '\n'.join(lyrs_params_list)
    return lyrs_params_str


#function to create the spps-params section of a params file
def _make_species_params_str(species=1):
    assert (isinstance(species, int) or
            (isinstance(species, list) and
             np.all([isinstance(item, dict) for item in species]))), (
                 "The 'species' param must be either an int (the number "
                 "of species parameters sections to include in the file) "
                 "or a list of dicts (one dict per species parameters "
                 "section to be included in the file, with keys and "
                 "values stipulating the aspects of the species to be "
                 "parameterized).")
    #create an empty list, to be filled with one params string per spp
    spps_params_list = []
    #if species is an integer, create a string of identical parameter sections
    if type(species) is int:
        #assert that it's an integer greater than 0
        assert species > 0, ("The number of Species to be created "
        "must be a positive integer.")
                #for each spp
        for i in range(species):
            #use genome params, but with no traits (i.e. string-format with a
            #zero-length str)
            genome_params = GENOME_PARAMS % (i, 'None', '')
            #add no change params
            change_params = ''
            #create the spp_params str, with no move_surf or disp_surf params
            spp_params_str = SPP_PARAMS % ("'spp_%i'" % i,
                                           i,
                                           '',
                                           i,
                                           i,
                                           i,
                                           '',
                                           '',
                                           genome_params,
                                           change_params,
                                           i,
                                          )
            #append to the spps_params_list
            spps_params_list.append(spp_params_str)

    #or if species is a list of dicts, then create individually customized
    #params sections for each Species
    elif type(species) is list:
        #create an empty list, to which any species who should have custom
        #gen-arch files created will be appended
        #assert that each item in the list is a dict
        assert False not in [type(item) is dict for item in species], ("If"
            " the 'species' argument is a list then it must contain only "
            "objects of type dict.")
        #create a lookup dict for the params strings for different spp params
        #sections
        params_str_dict = {
                        'msprime': {True: MSPRIME_INIT_PARAMS},
                        'move_surf': {True: MOVE_SURF_PARAMS},
                        'disp_surf': {True: DISP_SURF_PARAMS},
                        'genome': {True: GENOME_PARAMS,
                                   'custom': GENOME_PARAMS},
                        'dem_change': {True: SPP_DEM_CHANGE_EVENT_PARAMS},
                        'dem_events': {True: SPP_DEM_CHANGE_EVENTS_PARAMS},
                        'param_change': {True: SPP_PARAM_CHANGE_PARAMS},

                            }
        [v.update({False: ''}) for v in params_str_dict.values()]
        #for each spp
        for i, spp_dict in enumerate(species):
            #assert that the argument values are valid
            bool_args= ['movement_surface', 'genomes', 'parameter_change']
            int_args = ['n_traits', 'demographic_change', 'msprime_source_pops']
            for arg in bool_args:
                if arg in [*spp_dict]:
                    assert (type(spp_dict[arg]) is bool
                        or arg == 'genomes' and spp_dict[arg] == 'custom'), (
                        "The '%s' key in "
                        "each Species' dictionary must contain a "
                        "boolean value. But dict number %i in the "
                        "'species' argument contains a non-boolean "
                        "and otherwise invalid value.") % (arg, i)
            for arg in int_args:
                if arg in [*spp_dict]:
                    assert type(spp_dict[arg]) is int, ("The '%s' "
                        "key in each Species' dictionary must contain an "
                        "integer value. But dict number %i in the "
                        "'species' argument contains a non-integer "
                        "value:\n\n\t" "'%s': %s ") % (arg, i, arg,
                                                            str(spp_dict[arg]))
                    int_arg_str_fmt_dict = {'n_traits':'Traits',
                            'demographic_change': 'demographic change events',
                            'msprime_source_pops': 'msprime source populations',
                                           }
                    assert spp_dict[arg] >= 0, ("The number of %s to "
                                    "be created must be 0 or a positive "
                                    "integer.") % (int_arg_str_fmt_dict[arg])
            # get the msprime source pops params, if required
            if 'msprime_source_pops' in [*spp_dict]:
                print('making msprime params')
                n_pops = spp_dict['msprime_source_pops']
                msprime_params = params_str_dict['msprime'][n_pops>0]
                if n_pops > 0:
                    msprime_params = msprime_params % ('\n'.join([
                        MSPRIME_POP_PARAMS % i for i in range(n_pops)]))
            else:
                msprime_params = ''
            #get the movement surf and dispersal surf params, if required
            if 'movement_surface' in [*spp_dict]:
                ms_arg = spp_dict['movement_surface']
                move_surf_params = params_str_dict['move_surf'][ms_arg]
            else:
                move_surf_params = ''
            if 'dispersal_surface' in [*spp_dict]:
                ds_arg = spp_dict['dispersal_surface']
                disp_surf_params = params_str_dict['disp_surf'][ds_arg]
            else:
                disp_surf_params = ''
            #get the genome params, if required
            if 'genomes' in [*spp_dict] and spp_dict['genomes'] in [True,
                                                                    'custom']:
                #if this species should have a custom gen_arch_file made
                if spp_dict['genomes'] == 'custom':
                    gen_arch_file_str = ("'%%%%GEN_ARCH_FILE_STR%%%%_spp-%i_"
                                                            "gen_arch.csv'")
                    gen_arch_file_str = gen_arch_file_str % i
                    #make a tmp gen_arch_file for this spp
                    tmp_gen_arch_filename = '%i_%s.tmp' % (i,
                        str(np.random.randint(0, 10000)).zfill(5))
                    _make_custom_genomic_architecture_file(
                                                    tmp_gen_arch_filename)
                else:
                    gen_arch_file_str = 'None'
                #if this species' genomes should have traits
                if 'n_traits' in [*spp_dict] and spp_dict['n_traits'] > 0:
                    #get a list of params strings of length equal
                    #to the number of traits it should have
                    trait_params_list = []
                    for trt in range(spp_dict['n_traits']):
                        trait_params = TRT_PARAMS % (trt,
                                                "'trait_%i'" % trt, trt)
                        trait_params_list.append(trait_params)
                    #get the traits_params_str
                    traits_params = TRTS_PARAMS
                    #join the list into a single str and insert it into the
                    #traits_params_str
                    traits_params = traits_params % ''.join(trait_params_list)
                else:
                    traits_params = ''
                genome_params = params_str_dict['genome'][spp_dict['genomes']]
                genome_params = genome_params % (i, gen_arch_file_str,
                                                                traits_params)
            #or get empty str
            else:
                genome_params = ''
            #get the spp-change params (if either dem or param changes are
            #required
            if (('demographic_change' in [*spp_dict]
                 and spp_dict['demographic_change'])
                or ('parameter_change' in [*spp_dict]
                    and spp_dict['parameter_change'])):
                #create an empty string to tack either/both section(s) onto
                change_series_str = ''
                #tack on the dem-change events params str, if required
                if ('demographic_change' in [*spp_dict]
                    and spp_dict['demographic_change'] > 0):
                    dem_change_event_params_list = []
                    for n in range(spp_dict['demographic_change']):
                        params_str = params_str_dict['dem_change'][True]
                        dem_change_event_params_list.append(
                                                        params_str % (n, n))
                    events_series = ''.join(dem_change_event_params_list)
                    events_params_str = params_str_dict['dem_events'][True]
                    events_params_str = events_params_str % events_series
                    change_series_str = change_series_str + events_params_str
                #tack on the param-change params, if required
                if 'parameter_change' in [*spp_dict]:
                    param_change_arg = spp_dict['parameter_change']
                    param_change_params_str = (
                    params_str_dict['param_change'][param_change_arg])
                    events_params_str=events_params_str+param_change_params_str
                change_params = SPP_CHANGE_PARAMS
                change_params = change_params % (i, events_params_str)
            #or get empty str
            else:
                change_params = ''
            #get the overall spp params str for this spp
            spp_params_str = SPP_PARAMS % ("'spp_%i'" % i,
                                           i,
                                           msprime_params,
                                           i,
                                           i,
                                           i,
                                           move_surf_params,
                                           disp_surf_params,
                                           genome_params,
                                           change_params,
                                           i,
                                          )
            #append to the spps_params_list
            spps_params_list.append(spp_params_str)
    #join the whole list into one str
    spps_params_str = ''.join(spps_params_list)
    return spps_params_str


#function to create the data- and  stats-params sections of params file
#TODO: Add option for the argument to _make_parameters_file() to list the stats 
#to be calculated??
def _make_model_params_strs(section, arg=None):
    #assert the value of arg is valid
    assert arg in [True, False, None], ("The value of the '%s' argument "
        "provided for the model is not valid values. Value must be either a "
        "boolean or None.") % section
    if arg in [False, None]:
        return ''
    else:
        if section == 'data':
            return DATA_PARAMS
        elif section == 'stats':
            return STATS_PARAMS


#function to create a default params file, to be filled in by the user
def _make_params_file(filepath=None, layers=1, species=1, data=None,
        stats=None):
    '''<see docstring in gnx.make_parameters_file>'''
    lyrs_params_str = _make_lyrs_params_str(lyrs = layers)
    spps_params_str= _make_species_params_str(species = species)
    data_params_str = _make_model_params_strs('data', arg = data)
    stats_params_str = _make_model_params_strs('stats', arg = stats)
    #TODO DECIDE IF THIS SHOULD BE MADE OPTIONAL IN SOME WAY
    its_params_str = ITS_PARAMS
    #get the filepath
    if filepath is None:
        #get a string of the date and time
        datetime_str = time.strftime("%d-%m-%Y_%H:%M:%S", time.localtime())
        #and add a default filename
        filepath = 'GNX_params_%s.py' % datetime_str
    #check the filepath is pointed somewhere valid
    assert (os.path.isdir(os.path.split(filepath)[0])
            or os.path.split(filepath)[0] == ''), ("The filepath to which to "
            "write the parameters file does not point to a valid directory.")
    #coerce the file to a .py extension if it is not already provided
    filepath = os.path.splitext(filepath)[0] + '.py'
    #create the full params-file string
    file_str = PARAMS % (os.path.split(filepath)[1], lyrs_params_str,
        spps_params_str, its_params_str, data_params_str, stats_params_str)

    #add the gen_arch_file name and then create the files, if needed
    if re.search('%%GEN_ARCH_FILE_STR%%', file_str):
        #get the file prefix
        gen_arch_file_prefix = os.path.splitext(os.path.split(filepath)[1])[0]
        #rename the tmp files that were created
        tmp_files = [f for f in os.listdir('.') if (
            os.path.splitext(f)[1] == '.tmp')]
        for tmp_file in tmp_files:
            os.rename(tmp_file, gen_arch_file_prefix +
                '_spp-%s_gen_arch.csv' % (tmp_file.split('_')[0]))
        #replace the standin pattern with the file prefix
        file_str = re.sub('%%GEN_ARCH_FILE_STR%%', gen_arch_file_prefix,
                                                                file_str)

    #write the file_str to a "GNX_params_<datetime>.py" file
    with open(filepath, 'w') as f:
        f.write(file_str)


#function to recurse over the params dictionary 
#and return it as a Parameters_Dict object (i.e. a
#dict with k:v pairs as dynamic attributes)
def _make_params_dict(params):
    for k, v in params.items():
        method_names = ['clear', 'copy', 'fromkeys', 'get', 'items', 'keys',
                        'pop', 'popitem', 'setdefault', 'update', 'values']
        assert k not in method_names, ('The key "%s" in your params '
            'file is disallowed because it would clobber a Python method. '
            'Please edit name.\n\tNOTE: It holds the following value:'
            '\n%s' % (str(k), str(v)))
        if isinstance(v, dict):
            params[k] = _make_params_dict(params[k])
    params = _DynAttrDict(params)
    return(params)


#read a params file and return a ParametersDict object
def _read_params_file(filepath):
    #create a namespace to read the params dict into
    ns = {}
    #read and execute the file (to create a plain dict called 'params')
    exec(open(filepath, 'r').read(), ns)
    #get the params object from the namespace ns
    params = ns['params']
    #turn the params dict into a ParametersDict object
    params = ParametersDict(params)
    #set the model's name
    if 'name' in params['model'] and params['model']['name'] is not None:
        name = params['model']['name']
    else:
        #get the filename (minus path and extension) as the model name
        name = os.path.splitext(os.path.split(filepath)[-1])[0]
    params.model['name'] = name
    return(params)


#create a an empty custom gen-arch file for a species 
#(will be called if 'genomes':'custom' is a k:v pair in a 
#species dict fed into _make_paramters_file's 'species' argument
def _make_custom_genomic_architecture_file(filepath):
    #create the dataframe for the CSV file
    cols = ('locus', 'p', 'dom', 'r', 'trait', 'alpha')
    row0 = ([0], [np.nan], [np.nan], [np.nan], [np.nan], [np.nan])
    df_dict = dict(zip(cols, row0))
    df = pd.DataFrame.from_dict(df_dict)
    #write it to file, without the index
    df.to_csv(filepath, index = False)


