# -*- coding: utf-8 -*-
"""
Utility functions for PypeIt parameter sets
.. include:: ../include/links.rst
"""
import os
import time
import glob
from IPython import embed
import numpy as np
from astropy.table import Table
from configobj import ConfigObj
from pypeit import msgs, __version__
#-----------------------------------------------------------------------
# Parameter utility functions
#-----------------------------------------------------------------------
[docs]
def _eval_ignore():
"""Provides a list of strings that should not be evaluated."""
return [ 'open', 'file', 'dict', 'list', 'tuple' ]
[docs]
def eval_tuple(inp):
"""
Evaluate the input to one or more tuples.
This allows conversion of one or more tuples provided to a configuration
parameters.
.. warning::
- Currently can only handle simple components that can also be evaluated
(e.g., integers and floats).
Args:
inp (:obj:`list`):
A list of strings that are converted into a list of tuples. The
parentheses must be within the list of elements.
Return:
:obj:`list`: The list of tuples.
"""
joined = ','.join(inp)
try:
basic = eval(joined)
except:
msgs.error(f'Cannot evaluate {joined} into a valid tuple.')
# If any element of the basic evaulation is also a tuple, assume the result
# of the evaluation is a tuple of tuples. This is converted to a list.
return list(basic) if any([isinstance(e, tuple) for e in basic]) else [basic]
[docs]
def recursive_dict_evaluate(d):
"""
Recursively run :func:`eval` on each element of the provided
dictionary.
A raw read of a configuration file with `configobj`_ results in a
dictionary that contains strings or lists of strings. However, when
assigning the values for the various ParSets, the `from_dict`
methods expect the dictionary values to have the appropriate type.
E.g., the `configobj`_ will have something like d['foo'] = '1', when
the `from_dict` method expects the value to be an integer (d['foo']
= 1).
This function tries to evaluate *all* dictionary values, except for
those listed above in the :func:`_eval_ignore` function. Any value
in this list or where::
eval(d[k]) for k in d.keys()
raises an exception is returned as the original string.
This is currently only used in
:func:`~pypeit.par.pypeitpar.PypitPar.from_cfg_file`; see further comments
there.
Args:
d (dict):
Dictionary of values to evaluate
Returns:
dict: Identical to input dictionary, but with all string values
replaced with the result of `eval(d[k])` for all `k` in
`d.keys()`.
"""
ignore = _eval_ignore()
for k in d.keys():
if isinstance(d[k], dict):
# Recursive call to deal with nested dictionaries
d[k] = recursive_dict_evaluate(d[k])
continue
if isinstance(d[k], list) and any(['(' in e for e in d[k]]):
# NOTE: This enables syntax for constructing one or more tuples.
d[k] = eval_tuple(d[k])
continue
if isinstance(d[k], list):
replacement = []
for v in d[k]:
if v in ignore:
replacement += [ v ]
else:
try:
replacement += [ eval(v) ]
except:
replacement += [ v ]
d[k] = replacement
continue
try:
d[k] = eval(d[k]) if d[k] not in ignore else d[k]
except:
pass
return d
# TODO: I don't think this is used. We should deprecate it.
[docs]
def get_parset_list(cfg, pk, parsetclass):
"""
Create a list of ParSets based on a root keyword for a set of
defined groups in the configuration file.
For example, the :class:`InstrumentPar` group allows for a list of
detectors (:class:`DetectorPar`) with keywords like `detector1`,
`detector2`, etc. This function parses the provided configuration
object (`cfg`) to find any sections with `detector` (`pk`) as its
root. The remainder of the section name must be able to be
converted to an integer and the section itself must be able to setup
an instance of `parsetclass`. The sections must be number
sequentially from 1..N. E.g., the :class:`InstrumentPar`
configuration file cannot have `dectector1` and `detector3`, but no
`detector2`. The call to setup the detectors in the
:class:`InstrumentPar` is::
kwargs['detector'] = get_parset_list(cfg, 'detector', DetectorPar)
Args:
cfg (`configobj`_, :obj:`dict`):
The top-level configuration that defines a list of
sub-ParSets.
pk (str):
The root of the keywords used to set a list of sub-ParSets.
parsetclass (:class:`pypeit.par.parset.ParSet`):
The class used to construct each element in the list of
parameter subsets. The class **must** have a `from_dict`
method that instantiates the
:class:`pypeit.par.parset.ParSet` based on the provide
subsection/subdict from cfg.
Returns:
list: A list of instances of `parsetclass` parsed from the
provided configuration data.
Raises:
ValueError:
Raised if the indices of the subsections are not sequential
and 1-indexed.
"""
# Get the full list of keys
k = cfg.keys()
# Iterate through the list of keys to find the appropriate sub
# parameter sets and their order.
par = []
order = []
for _k in k:
if _k == pk and cfg[_k] is None:
continue
if pk in _k:
try:
# Get the order for this subgroup (e.g., 2 for
# 'detector2'
order += [ int(_k.replace(pk,'')) ]
# And instantiate the parameter set
par += [ parsetclass.from_dict(cfg[_k]) ]
except:
continue
if len(par) > 0:
# Make sure the instances are correctly sorted and sequential
srt = np.argsort(order)
if np.any(np.array(order)[srt]-1 != np.arange(order[srt[-1]])):
raise ValueError('Parameter set series must be sequential and 1-indexed.')
# Return the sorted instances
return [par[i] for i in srt]
# No such subsets were defined, so return a null result
return None
[docs]
def parset_to_dict(par):
"""
Convert the provided parset into a dictionary.
Args:
par (ParSet):
Returns:
dict: Converted ParSet
"""
try:
d = dict(ConfigObj(par.to_config(section_name='tmp'))['tmp'])
except:
d = dict(ConfigObj(par.to_config()))
return recursive_dict_evaluate(d)