configuration.py 10.7 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
3
# Copyright (C) 2012, 2013 Centre de données Astrophysiques de Marseille
# Licensed under the CeCILL-v2 licence - see Licence_CeCILL_V2-en.txt
Yannick Roehlly's avatar
Yannick Roehlly committed
4
# Author: Yannick Roehlly
5
6

import pkgutil
7
import collections
8
import multiprocessing as mp
9
from textwrap import wrap
10
11
12
13
14
15

import configobj
from glob import glob  # To allow the use of glob() in "eval..."
import pkg_resources
import numpy as np

16
from ..data import Database
17
from ..utils import read_table
18
19
from .. import creation_modules
from .. import analysis_modules
20
21
22
23
24


def list_modules(package_name):
    """Lists the modules available in a package

Yannick Roehlly's avatar
Yannick Roehlly committed
25
    Parameters
26
    ----------
27
    package_name: string
28
        Name of the package (e.g. pcigale.creation_modules).
29
30
31

    Returns
    -------
32
    module_name: array of strings
33
34
35
36
37
38
39
40
41
        List of the available modules.

    """
    directory = pkg_resources.resource_filename(package_name, '')
    module_names = [name for _, name, _ in pkgutil.iter_modules([directory])]

    return module_names


Yannick Roehlly's avatar
Yannick Roehlly committed
42
43
44
45
46
47
def evaluate_description(description):
    """Evaluate a description from the config file as a list.

    The description is read from the config file by configobj that transforms
    coma separated value in a list. From this description, this function try
    to evaluate the desired list of values:
48
49
50
    - If the description is a string beginning with 'eval ', then its content
      (without 'eval ') is evaluated as Python code and its result returned.
      An array is expected.
51
52
53
    - If the description is a string beginning by 'range', the start, step and
      stop values are then expected and the range is evaluated (stop included
      if reached.
Yannick Roehlly's avatar
Yannick Roehlly committed
54
55
    - Then the function tries to evaluate the description as a Numpy array of
      float and returns the mere list if this fails.
56

Yannick Roehlly's avatar
Yannick Roehlly committed
57
    Parameters
58
    ----------
59
    description: string or list
60
61
62
63
        The description to be evaluated.

    Returns
    -------
64
     results: list
Yannick Roehlly's avatar
Yannick Roehlly committed
65
        The evaluated list of values.
66
67

    """
68
69
70
71
72
73
74
75
76
77
78
79
80
81
    results = description
    if type(description) == str:
        if description.startswith('eval '):
            results = eval(description[4:])
            # If the evaluation lead to a single value, we put it in a list.
            if not isinstance(results, collections.Iterable):
                results = [results]
        elif description.startswith('range '):
            start, stop, step = [float(item) for item
                                 in description[5:].split()]
            results = np.arange(start, stop+step, step)
        else:
            # We need to return a list to combine the list of possible values
            # for each parameter.
82
            results = [results]
83
84
85
86
87
88
89

    # We prefer to evaluate the parameter as a numpy array of floats if
    # possible.
    try:
        results = np.array(results, float)
    except ValueError:
        pass
90

Yannick Roehlly's avatar
Yannick Roehlly committed
91
    return results
92
93
94
95
96
97
98
99
100


class Configuration(object):
    """This class manages the configuration of pcigale.
    """

    def __init__(self, filename="pcigale.ini"):
        """Initialise a pcigale configuration.

Yannick Roehlly's avatar
Yannick Roehlly committed
101
        Parameters
102
        ----------
103
        filename: string
104
105
106
107
108
            Name of the configuration file (pcigale.conf by default).

        """
        self.config = configobj.ConfigObj(filename,
                                          write_empty_values=True,
109
110
                                          indent_type='  ',
                                          encoding='UTF8')
111
112
113
114
115
116
117
118
119
120
121
122

    def create_blank_conf(self):
        """Create the initial configuration file

        Write the initial pcigale configuration file where the user can state
        which data file to use, which modules to use for the SED creation, as
        well as the method selected for statistical analysis.

        """

        self.config['data_file'] = ""
        self.config.comments['data_file'] = wrap(
123
124
125
126
127
128
            "File containing the input data. The columns are 'id' (name of the"
            " object), 'redshift' (if 0 the distance is assumed to be 10 pc), "
            "the filter names for the fluxes, and the filter names with the "
            "'_err' suffix for the uncertainties. The fluxes and the "
            "uncertainties must be in mJy. This file is optional to generate "
            "the configuration file, in particular for the savefluxes module.")
129

130
        self.config['creation_modules'] = []
131
        self.config.comments['creation_modules'] = [""] + wrap(
Médéric Boquien's avatar
Médéric Boquien committed
132
            "Order of the modules use for SED creation. Available modules: "
133
            "SFH: sfh2exp, sfhdelayed, sfhfromfile, sfhperiodic ; "
134
135
136
            "SSP: bc03, m2005 ; "
            "Nebular: nebular ; "
            "Attenuation: dustatt_calzleit, dustatt_powerlaw ; "
137
            "Dust model: casey2012, dale2014, dl2007, dl2014 ; "
138
            "AGN: dale2014, fritz2006 ; "
Médéric Boquien's avatar
Médéric Boquien committed
139
            "Radio: radio ; "
140
            "redshift: redshifting (mandatory!).")
141
142
143
144

        self.config['analysis_method'] = ""
        self.config.comments['analysis_method'] = [""] + wrap(
            "Method used for statistical analysis. Available methods: "
145
            "pdf_analysis, savefluxes.")
146

147
148
149
150
151
        self.config['cores'] = ""
        self.config.comments['cores'] = [""] + wrap(
            "Number of CPU cores available. This computer has {} cores."
            .format(mp.cpu_count()))

152
153
154
155
156
157
158
159
160
161
162
163
        self.config.write()

    def generate_conf(self):
        """Generate the full configuration file

        Reads the user entries in the initial configuration file and add the
        configuration options of all selected modules as well as the filter
        selection based on the filters identified in the data table file.

        """

        # Getting the list of the filters available in pcigale database
164
        with Database() as base:
165
            filter_list = base.get_filter_names()
166

167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
        if self.config['data_file'] != '':
            obs_table = read_table(self.config['data_file'])

            # Check that the id and redshift columns are present in the input
            # file
            if 'id' not in obs_table.columns:
                raise Exception("Column id not present in input file")
            if 'redshift' not in obs_table.columns:
                raise Exception("Column redshift not present in input file")

            # Finding the known filters in the data table
            column_list = []
            for column in obs_table.columns:
                filter_name = column[:-4] if column.endswith('_err') else column
                if filter_name in filter_list:
                    column_list.append(column)

            # Check that we don't have an error column without the associated
            # flux
            for column in column_list:
Médéric Boquien's avatar
Médéric Boquien committed
187
188
                if column.endswith('_err') and (column[:-4]
                                                not in column_list):
189
190
                    raise Exception("The observation table as a {} column "
                                    "but no {} column.".format(column,
Médéric Boquien's avatar
Médéric Boquien committed
191
                                                               column[:-4]))
192
193
194
195

            self.config['column_list'] = column_list
        else:
            self.config['column_list'] = ''
196
197
198
199
200
        self.config.comments['column_list'] = [""] + wrap(
            "List of the columns in the observation data file to use for "
            "the fitting.")

        # SED creation modules configurations. For each module, we generate
Yannick Roehlly's avatar
Yannick Roehlly committed
201
        # the configuration section from its parameter list.
202
203
204
205
        self.config['sed_creation_modules'] = {}
        self.config.comments['sed_creation_modules'] = ["", ""] + wrap(
            "Configuration of the SED creation modules.")

206
        for module_name in self.config['creation_modules']:
207
208
209
            self.config["sed_creation_modules"][module_name] = {}
            sub_config = self.config["sed_creation_modules"][module_name]

210
            for name, (typ, description, default) in \
211
212
213
                    creation_modules.get_module(
                        module_name,
                        blank=True).parameter_list.items():
214
215
216
217
218
219
                if default is None:
                    default = ''
                sub_config[name] = default
                sub_config.comments[name] = wrap(description)

            self.config['sed_creation_modules'].comments[module_name] = [
220
                creation_modules.get_module(module_name, blank=True).comments]
221
222
223
224
225
226

        # Configuration for the analysis method
        self.config['analysis_configuration'] = {}
        self.config.comments['analysis_configuration'] = ["", ""] + wrap(
            "Configuration of the statistical analysis method.")
        module_name = self.config['analysis_method']
227
        for name, (typ, desc, default) in \
228
                analysis_modules.get_module(module_name).parameter_list.items():
229
230
231
232
233
234
            if default is None:
                default = ''
            self.config['analysis_configuration'][name] = default
            self.config['analysis_configuration'].comments[name] = wrap(desc)

        self.config.write()
Yannick Roehlly's avatar
Yannick Roehlly committed
235

236
237
    @property
    def configuration(self):
Yannick Roehlly's avatar
Yannick Roehlly committed
238
        """Returns a dictionary for the session configuration.
Yannick Roehlly's avatar
Yannick Roehlly committed
239
240
241

        Returns
        -------
242
        configuration['data_file']: string
Yannick Roehlly's avatar
Yannick Roehlly committed
243
            File containing the observations to fit.
244
        configuration['column_list']: list of strings
Yannick Roehlly's avatar
Yannick Roehlly committed
245
            List of the columns of data_file to use in the fitting.
246
        configuration['creation_modules']: list of strings
Yannick Roehlly's avatar
Yannick Roehlly committed
247
            List of the modules (in the right order) used to create the SEDs.
248
        configuration['creation_modules_params']: list of dictionaries
Yannick Roehlly's avatar
Yannick Roehlly committed
249
250
            Configuration parameters for each module. To each parameter, the
            dictionary associates a list of possible values (possibly only
Yannick Roehlly's avatar
Yannick Roehlly committed
251
            one).
252
        configuration['analysis_method']: string
Yannick Roehlly's avatar
Yannick Roehlly committed
253
            Statistical analysis module used to fit the data.
254
        configuration['analysis_method_params']: dictionary
Yannick Roehlly's avatar
Yannick Roehlly committed
255
            Parameters for the statistical analysis module. To each parameter
Yannick Roehlly's avatar
Yannick Roehlly committed
256
257
258
259
            is associated a list of possible values.
        """
        configuration = {}

260
        for section in ['data_file', 'column_list', 'creation_modules',
261
                        'analysis_method']:
Yannick Roehlly's avatar
Yannick Roehlly committed
262
            configuration[section] = self.config[section]
263
        configuration['cores'] = int(self.config['cores'])
Yannick Roehlly's avatar
Yannick Roehlly committed
264

Yannick Roehlly's avatar
Yannick Roehlly committed
265
        # Parsing the SED modules parameters
266
267
        configuration['creation_modules_params'] = []
        for module in self.config['creation_modules']:
268
            module_params = {}
Yannick Roehlly's avatar
Yannick Roehlly committed
269
270
            for key, value in \
                    self.config['sed_creation_modules'][module].items():
Yannick Roehlly's avatar
Yannick Roehlly committed
271
                module_params[key] = evaluate_description(value)
272
            configuration['creation_modules_params'].append(module_params)
273
274

        # Analysis method parameters
275
276
        configuration['analysis_method_params'] = \
            self.config['analysis_configuration']
Yannick Roehlly's avatar
Yannick Roehlly committed
277
278

        return configuration