parameters.py 8.76 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
        self.size = len(table)
181
        self.modules = conf['sed_modules']
182 183
        self.blocks = self._split(range(self.size),
                                  conf['analysis_params']['blocks'])
184

185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
        # 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
200
                                                table[colname]]
201 202 203
                    else:
                        dict_params[parname] = list(table[colname])
            self.parameters.append(dict_params)
204 205 206

        del table

207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
    @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.")


227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    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
244
                   self.parameters[idx_module]} for idx_module, module
245 246 247 248
                  in enumerate(self.modules)]

        return params

249
    def index_module_changed(self, idx1, idx2):
250 251 252 253
        """Find the index of the first module affected by a change of parameters.

        Parameters
        ----------
254
        idx1: int
255
            First index
256
        idx2: int
257 258 259 260 261 262 263 264 265
            Second index

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

        """

266 267 268
        for module_idx, module in enumerate(self.parameters):
            if any([param[idx1] != param[idx2] for param in module.values()]):
                return module_idx
269

270
        return len(self.parameters)