__all__ = ['generate_astropy_model']
from astropy.modeling import models
from eispac.core.eisfittemplate import EISFitTemplate
# if __name__ == '__main__':
# # Import local versions of submodules
# print('Notice: Loading local version of eispac.read_template')
# from read_template import EISFitTemplate, read_template
# else:
# # Import from installed package
# from eispac.read_template import EISFitTemplate, read_template
[docs]
def generate_astropy_model(template):
"""Create an Astropy fitting model using a template intended for MPFIT.
Parameters
----------
template : EISFitTemplate object or string
Either a single MPFIT-style template object or a string containing the
full path to a template file. The template MUST contain both 'template'
and 'parinfo' attributes.
Returns
-------
combined_model : CompoundModel object from Astropy.modeling
A fully initialized CompoundModel object with the same parameter values
and constraints (including tied parameter functions) as given in the
'parinfo' attribute of the input template.
"""
# Define list of forbidden strings in "tied" parameter expressions.
# This should improve security and reduce the scope of possible abuse.
black_list = ['import', 'def', 'class', '@', 'sys.', 'os.', 'subprocess.']
# Validate input and, if needed, load the template file
if isinstance(template, str):
use_template = EISFitTemplate.read_template(template, quiet=True)
elif isinstance(template, EISFitTemplate):
use_template = template
else:
raise TypeError('Please input either a valid template filename'
' or an EISFitTemplate object.')
# Extract basic model information from the template
parinfo = use_template.parinfo
num_gauss = use_template.template['n_gauss']
poly_degree = use_template.template['n_poly'] - 1 # n_poly is just number of terms
num_params = len(parinfo)
num_subcomp = num_gauss + 1 if poly_degree > -1 else num_gauss
# Create each of the component models and add them together
m_set = [] # list of individual astropy fitting models
p = 0 # Counter for current parinfo parameter number
for m in range(num_subcomp):
if m < num_gauss:
# Gaussian component with values of (amplitude, mean, stddev)
m_set.append(models.Gaussian1D(parinfo[p]['value'],
parinfo[p+1]['value'],
parinfo[p+2]['value']))
elif m == num_gauss and poly_degree > -1:
# Polynomial background profile
# TODO: double check the order of the template polynomial coeffs
poly_coeffs = {'c'+str(poly_degree-i) : parinfo[p+i]['value']
for i in range(poly_degree+1)}
m_set.append(models.Polynomial1D(degree=poly_degree,
domain=None, window=None,
**poly_coeffs))
# loop over all model parameters and set basic constraints
for param in m_set[m].param_names:
if parinfo[p]['fixed'] == 1:
m_set[m].__dict__[param].fixed = True
if parinfo[p]['limited'][0] == 1:
m_set[m].__dict__[param].min = parinfo[p]['limits'][0]
if parinfo[p]['limited'][1] == 1:
m_set[m].__dict__[param].max = parinfo[p]['limits'][1]
p += 1
# Add the new model to the combined model
if m == 0:
combined_model = m_set[m]
else:
combined_model = combined_model + m_set[m]
# Check for tied parameters and convert MPFIT-style strings into functions
generic_names = ['p['+str(i)+']' for i in range(num_params)]
param_names = combined_model.param_names
for p in range(num_params):
tied_str = str(parinfo[p]['tied'])
if any(gname in tied_str for gname in generic_names):
if any(forbidden in tied_str for forbidden in black_list):
raise ValueError('Unsupported code found in tied parameter'
' string. \nReminder: only simple mathematical'
' expressions and generic parameter names are'
' permitted.')
tied_str = tied_str.replace('^', '**') # Translate pow notation
for g in range(num_params):
tied_str = tied_str.replace(generic_names[g], 'model.'+param_names[g])
tied_func = eval('lambda model : '+tied_str)
combined_model.__dict__[param_names[p]].tied = tied_func
return combined_model