configuration.py 10.6 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

Médéric Boquien's avatar
Médéric Boquien committed
6
from astropy.table import Table
7
8
9
import configobj
import pkg_resources
import pkgutil
10
import collections
11
import itertools
12
import numpy as np
13
from glob import glob # To allow the use of glob() in "eval..."
14
from textwrap import wrap
15
from .tools import param_dict_combine
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
32
33
34
35
36
37
38
39
40
41

    Returns
    -------
    module_name : array of strings
        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
    ----------
Yannick Roehlly's avatar
Yannick Roehlly committed
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
        ----------
        filename : string
            Name of the configuration file (pcigale.conf by default).

        """
        self.config = configobj.ConfigObj(filename,
                                          write_empty_values=True,
                                          indent_type='  ')

    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(
            "File containing the observation data to be fitted. Each flux "
            "column must have the name of the corresponding filter, the "
            "error columns are suffixed with '_err'. The values must be "
            "in mJy.")

127
128
        self.config['creation_modules'] = []
        self.config.comments['creation_modules'] = [""] + wrap(
129
            "Order of the modules use for SED creation. Available modules : "
130
            + ', '.join(list_modules('pcigale.creation_modules')) + ".")
131
132
133
134

        self.config['analysis_method'] = ""
        self.config.comments['analysis_method'] = [""] + wrap(
            "Method used for statistical analysis. Available methods: "
135
            + ', '.join(list_modules('pcigale.analysis_modules')) + ".")
136
137
138
139
140
141
142
143
144
145
146
147
148

        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
149
150
        with Database() as base:
            filter_list = base.get_filter_list()[0]
151
152

        # Finding the known filters in the data table
153
        obs_table = read_table(self.config['data_file'])
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
        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:
            if column.endswith('_err') and (column[:-4] not in column_list):
                raise StandardError("The observation table as a {} column "
                                    "but no {} column.".format(column,
                                                               column[:-4]))

        self.config['column_list'] = column_list
        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
173
        # the configuration section from its parameter list.
174
175
176
177
        self.config['sed_creation_modules'] = {}
        self.config.comments['sed_creation_modules'] = ["", ""] + wrap(
            "Configuration of the SED creation modules.")

178
        for module_name in self.config['creation_modules']:
179
180
181
            self.config["sed_creation_modules"][module_name] = {}
            sub_config = self.config["sed_creation_modules"][module_name]

182
            for name, (typ, description, default) in \
183
184
185
                    creation_modules.get_module(
                        module_name,
                        blank=True).parameter_list.items():
186
187
188
189
190
191
                if default is None:
                    default = ''
                sub_config[name] = default
                sub_config.comments[name] = wrap(description)

            self.config['sed_creation_modules'].comments[module_name] = [
192
                creation_modules.get_module(module_name, blank=True).comments]
193
194
195
196
197
198

        # 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']
199
        for name, (typ, desc, default) in \
200
                analysis_modules.get_module(module_name).parameter_list.items():
201
202
203
204
205
206
            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
207

208
209
    @property
    def configuration(self):
Yannick Roehlly's avatar
Yannick Roehlly committed
210
        """Returns a dictionary for the session configuration.
Yannick Roehlly's avatar
Yannick Roehlly committed
211
212
213
214
215
216
217

        Returns
        -------
        configuration['data_file'] : string
            File containing the observations to fit.
        configuration['column_list'] : list of strings
            List of the columns of data_file to use in the fitting.
218
        configuration['creation_modules'] : list of strings
Yannick Roehlly's avatar
Yannick Roehlly committed
219
            List of the modules (in the right order) used to create the SEDs.
220
        configuration['creation_modules_params'] : list of dictionaries
Yannick Roehlly's avatar
Yannick Roehlly committed
221
222
            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
223
224
225
            one).
        configuration['analysis_method'] : string
            Statistical analysis module used to fit the data.
Yannick Roehlly's avatar
Yannick Roehlly committed
226
227
        configuration['analysis_method_params'] : dictionary
            Parameters for the statistical analysis module. To each parameter
Yannick Roehlly's avatar
Yannick Roehlly committed
228
229
230
231
            is associated a list of possible values.
        """
        configuration = {}

232
        for section in ['data_file', 'column_list', 'creation_modules',
233
                        'analysis_method']:
Yannick Roehlly's avatar
Yannick Roehlly committed
234
235
            configuration[section] = self.config[section]

Yannick Roehlly's avatar
Yannick Roehlly committed
236
        # Parsing the SED modules parameters
237
238
        configuration['creation_modules_params'] = []
        for module in self.config['creation_modules']:
Yannick Roehlly's avatar
Yannick Roehlly committed
239
            module_params = {}
Yannick Roehlly's avatar
Yannick Roehlly committed
240
241
            for key, value in \
                    self.config['sed_creation_modules'][module].items():
Yannick Roehlly's avatar
Yannick Roehlly committed
242
                module_params[key] = evaluate_description(value)
243
            configuration['creation_modules_params'].append(module_params)
244
245

        # Analysis method parameters
246
247
        configuration['analysis_method_params'] = \
            self.config['analysis_configuration']
Yannick Roehlly's avatar
Yannick Roehlly committed
248
249

        return configuration
250
251

    @property
252
    def creation_modules_conf_array(self):
Yannick Roehlly's avatar
Yannick Roehlly committed
253
        """Return the array of all the possible parameter sets from the
254
255
256
        SED creation modules.

        TODO: Maybe it would be more optimal to create an iterator that would
Yannick Roehlly's avatar
Yannick Roehlly committed
257
              iterate over the whole parameter combinations instead of
258
259
260
261
262
              creating the array.

        Returns
        -------
        result : array of arrays of dictionaries
Yannick Roehlly's avatar
Yannick Roehlly committed
263
            The inner arrays contains the various parameter dictionaries
264
            for the modules listed in configuration['creation_modules'].
265
266
267
268

        """

        # First, for each module, we transform the dictionary containing all
Yannick Roehlly's avatar
Yannick Roehlly committed
269
270
        # the possible value for each parameter in a list of dictionaries
        # containing one value for each parameter. We put this list in a list
271
272
        # corresponding to the SED modules one.
        tmp_list = [param_dict_combine(dictionary) for dictionary in
273
                    self.configuration['creation_modules_params']]
274
275
276
277

        # The we use itertools to create an array of all possible
        # combinations.
        return [x for x in itertools.product(*tmp_list)]