parameters.py 8.92 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
# -*- coding: utf-8 -*-
# Copyright (C) 2014 University of Cambridge
# Copyright (C) 2016 Universidad de Antofagasta
# Licensed under the CeCILL-v2 licence - see Licence_CeCILL_V2-en.txt
# Author: Médéric Boquien

import collections
import itertools
import numpy as np

11
from ..utils import read_table
12
from ..warehouse import SedWarehouse
13

14
15
class ParametersManager(object):
    """Class to abstract the call to the relevant parameters manager depending
16
17
18
    how the physical parameters of the models are provided (directly in the
    pcigale.ini file ).

19
    A ParametersManager allows to generate a list containing the parameters for
20
21
22
23
24
25
    a modules whose index is passed as an argument. It also allows to know what
    modules have changed their parameters given their indices. Because the
    order of the modules is optimised to minimise the computations, this allows
    to keep the cache of partially computed models in SedWarehouse as small as
    possible by weeding out partial models that will not be used anymore."""

26
27
28
    def __new__(object, conf):
        if conf['parameters_file']:
            return ParametersManagerFile(conf)
29
        else:
30
            return ParametersManagerGrid(conf)
31
32


33
34
class ParametersManagerGrid(object):
    """Class to generate a parameters manager for a systematic grid using the
35
36
    parameters given in the pcigale.ini file."""

37
    def __init__(self, conf):
38
39
40
41
        """Instantiate the class.

        Parameters
        ----------
42
        conf: dictionary
43
44
45
            Contains the modules in the order they are called

        """
46
47
        self.modules = conf['sed_modules']
        self.parameters = [self._param_dict_combine(conf['sed_modules_params'][module])
48
                           for module in self.modules]
49
50
        self.shape = tuple(len(parameter) for parameter in self.parameters)
        self.size = int(np.product(self.shape))
51
52
53
54
55
56
57
58
59
        if 'blocks' not in conf['analysis_params']:
            conf['analysis_params']['blocks'] = 1
        self.blocks = self._split(range(self.size),
                                  conf['analysis_params']['blocks'],
                                  len(conf['sed_modules_params']['redshifting']['redshift']))

    def __len__(self):
        return self.size

60
61
    @staticmethod
    def _split(l, nb, nz):
62
63
64
65
66
67
68
69
70
71
72
73
74
        """Split a list l into nb blocks with blocks of such size that all the
        redshifts of a given model as in the same block.

        Parameters
        ----------
        l: list
            List to split.
        nb: int
            Number of blocks.
        nz: int
            Number of redshifts.

        """
75
76
        k = len(l) // nb
        step = k + nz - k%nz
77
78

        return [l[i * step: (i+1) * step] for i in range(nb)]
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

    def _param_dict_combine(self, dictionary):
        """Given a dictionary associating to each key an array, returns all the
        possible dictionaries associating a single element to each key.

        Parameters
        ----------
        dictionary: dict
            Dictionary associating an array to its (or some of its) keys.

        Returns
        -------
        combination_list: list of dictionaries
            List of dictionaries with the same keys but associating one element
            to each.

        """
        # We make a copy of the dictionary as we are modifying it.
97
        dictionary = collections.OrderedDict(dictionary)
98
99
100
101
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140

        # First, we must ensure that all values are lists; when a value is a
        # single element, we put it in a list.
        # We must take a special care of strings, because they are iterable.

        for key, value in dictionary.items():
            if ((not isinstance(value, collections.Iterable)) or
                    isinstance(value, str)):
                dictionary[key] = [value]

        # We use itertools.product to make all the possible combinations from
        # the value lists.
        key_list = dictionary.keys()
        value_array_list = [dictionary[key] for key in key_list]
        combination_list = [dict(zip(key_list, combination))
                            for combination in
                            itertools.product(*value_array_list)]

        return combination_list

    def from_index(self, index):
        """Provides the parameters of a model given a 1D index.

        Parameters
        ----------
        index: int
            1D index of the model for which we want the parameters

        Returns
        -------
        params: list
            Parameters of the model corresponding to the index

        """
        # Our problem is isomorph to the conversion between a linear and an nD
        # index of an nD array. Thankfully numpy's unravel_index does the
        # conversion from a 1D index to nD indices.
        indices = np.unravel_index(index, self.shape)
        params = [self.parameters[module][param_idx]
                  for module, param_idx in enumerate(indices)]

        return params

141
    def index_module_changed(self, idx1, idx2):
142
143
144
145
        """Find the index of the first module affected by a change of parameters.

        Parameters
        ----------
146
        idx1: int
147
            First index
148
        idx2: int
149
150
151
152
153
154
155
156
            Second index

        Returns
        -------
        module_idx: int
            Index of the first module that has a different parameter

        """
157
        indices = np.unravel_index((idx1, idx2), self.shape)
158
159
160
161
162
        for module_idx, (i, j) in enumerate(indices):
            if i != j:
                return module_idx

        return len(self.shape)
163
164


165
166
class ParametersManagerFile(object):
    """Class to generate a parameters manager for list of parameters given in an
167
168
    input file."""

169
    def __init__(self, conf):
170
171
172
173
        """Instantiate the class.

        Parameters
        ----------
174
        conf: dictionary
175
176
177
            Contains the name of the file containing the parameters

        """
178
        table = read_table(conf['parameters_file'])
179

180
181
        self.size = len(table)

182
183
184
185
186
        self.modules = []
        for colname in table.colnames:
            module = colname.split('.', 1)[0]
            if module not in self.modules:
                self.modules.append(module)
187
188
        self.blocks = self._split(range(self.size),
                                  conf['analysis_params']['blocks'])
189

190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
        # The parameters file is read using astropy.Table. Unfortunately, it
        # stores strings as np.str_, which is not marshalable, which means we
        # cannot use the list of parameter to build a key for the cache. This
        # overcome this, rather than using the table directly, we split it into
        # several dictionaries, one for each module. Each dictionary contains
        # the arguments in the form of lists that we convert to the right type:
        # float for numbers and str for strings.
        self.parameters = []
        for module in self.modules:
            dict_params = {}
            for colname in table.colnames:
                if colname.startswith(module):
                    parname = colname.split('.', 1)[1]
                    if type(table[colname][0]) is np.str_:
                        dict_params[parname] = [str(val) for val in
205
                                                table[colname]]
206
207
208
                    else:
                        dict_params[parname] = list(table[colname])
            self.parameters.append(dict_params)
209
210
211

        del table

212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
    @staticmethod
    def _split(l, nb):
        """Split a list l into nb blocks.

        Parameters
        ----------
        l: list
            List to split.
        nb: int
            Number of blocks.

        """
        step = len(l) // nb
        if step > 0:
            return [l[i * step: (i+1) * step] for i in range(nb)]

        raise ValueError("The number of blocks must be no more than the number"
                         "of models.")


232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
    def from_index(self, index):
        """Provides the parameters of a model given an index.

        Parameters
        ----------
        index: int
            index of the model for which we want the parameters

        Returns
        -------
        params: list
            Parameters of the model corresponding to the index

        """

        # As we have a simple file, this corresponds to the line number
        params = [{name: self.parameters[idx_module][name][index] for name in
249
                   self.parameters[idx_module]} for idx_module, module
250
251
252
253
                  in enumerate(self.modules)]

        return params

254
    def index_module_changed(self, idx1, idx2):
255
256
257
258
        """Find the index of the first module affected by a change of parameters.

        Parameters
        ----------
259
        idx1: int
260
            First index
261
        idx2: int
262
263
264
265
266
267
268
269
270
            Second index

        Returns
        -------
        module_idx: int
            Index of the first module that has a different parameter

        """

271
272
273
        for module_idx, module in enumerate(self.parameters):
            if any([param[idx1] != param[idx2] for param in module.values()]):
                return module_idx
274

275
        return len(self.parameters)