ferre 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ferre-1.0.0/PKG-INFO +11 -0
- ferre-1.0.0/README.rst +12 -0
- ferre-1.0.0/python/ferre/__init__.py +1 -0
- ferre-1.0.0/python/ferre/ferre.py +1045 -0
- ferre-1.0.0/python/ferre.egg-info/PKG-INFO +11 -0
- ferre-1.0.0/python/ferre.egg-info/SOURCES.txt +8 -0
- ferre-1.0.0/python/ferre.egg-info/dependency_links.txt +1 -0
- ferre-1.0.0/python/ferre.egg-info/top_level.txt +1 -0
- ferre-1.0.0/setup.cfg +4 -0
- ferre-1.0.0/setup.py +94 -0
ferre-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ferre
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Spectral analysis code with synthetic spectral grid
|
|
5
|
+
Home-page: https://github.com/dnidever/ferre
|
|
6
|
+
Author: Carlos Allende Prieto, David Nidever
|
|
7
|
+
Author-email: carlos.allende.prieto@iac.es,dnidever@montana.edu
|
|
8
|
+
Requires: numpy
|
|
9
|
+
Requires: astropy(>=4.0)
|
|
10
|
+
Requires: scipy
|
|
11
|
+
Requires: matplotlib
|
ferre-1.0.0/README.rst
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
ferre
|
|
2
|
+
=====
|
|
3
|
+
|
|
4
|
+
FERRE is a data analysis code written in FORTRAN90. It matches models to data, taking a
|
|
5
|
+
set of observations and identifying the model parameters that best reproduce the data, in a chi-
|
|
6
|
+
squared sense. Model predictions are to be given as an array whose values are a function of the
|
|
7
|
+
model parameters, i.e. numerically. FERRE holds this array in memory, or in a direct-access
|
|
8
|
+
binary file, and interpolates in it. The code returns, in addition to the optimal set of parameters,
|
|
9
|
+
their error covariance, and the corresponding model prediction.
|
|
10
|
+
|
|
11
|
+
The FERRE fortran code was written by Carlos Allende Prieto. David Nidever repackaged it, added
|
|
12
|
+
some Python code to call the Fortran program and added it to Pypi so it can be pip installed.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = ['ferre']
|
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import copy
|
|
3
|
+
import numpy as np
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
import traceback
|
|
7
|
+
import shutil
|
|
8
|
+
from dlnpyutils import utils as dln
|
|
9
|
+
from scipy.optimize import curve_fit
|
|
10
|
+
from theborg import emulator
|
|
11
|
+
from astropy.table import Table
|
|
12
|
+
from doppler.spec1d import Spec1D,continuum
|
|
13
|
+
from . import utils
|
|
14
|
+
|
|
15
|
+
cspeed = 2.99792458e5 # speed of light in km/s
|
|
16
|
+
|
|
17
|
+
def gridinfo(filename):
|
|
18
|
+
""" Get information about the FERRE grid."""
|
|
19
|
+
|
|
20
|
+
if os.path.exists(filename)==False:
|
|
21
|
+
raise FileNotFoundError(filename)
|
|
22
|
+
|
|
23
|
+
# Read the header information
|
|
24
|
+
f = open(filename,'r')
|
|
25
|
+
header = []
|
|
26
|
+
line = f.readline().strip()
|
|
27
|
+
while line != '/':
|
|
28
|
+
header.append(line)
|
|
29
|
+
line = f.readline().strip()
|
|
30
|
+
f.close()
|
|
31
|
+
|
|
32
|
+
out = {'header':header}
|
|
33
|
+
|
|
34
|
+
header = np.char.array(header)
|
|
35
|
+
|
|
36
|
+
# Parse the information
|
|
37
|
+
for h in header[1:]:
|
|
38
|
+
dum = h.split()
|
|
39
|
+
k = dum[0]
|
|
40
|
+
val = dum[2:]
|
|
41
|
+
if k.startswith('COMMENT'):
|
|
42
|
+
val = ' '.join(val)
|
|
43
|
+
else:
|
|
44
|
+
orig = val.copy()
|
|
45
|
+
val = []
|
|
46
|
+
for v in orig:
|
|
47
|
+
v = v.replace("'","")
|
|
48
|
+
if dln.isnumber(v):
|
|
49
|
+
v = float(v)
|
|
50
|
+
elif v.isnumeric():
|
|
51
|
+
v = int(v)
|
|
52
|
+
val.append(v)
|
|
53
|
+
if len(val)==1:
|
|
54
|
+
val = val[0]
|
|
55
|
+
out[k] = val
|
|
56
|
+
|
|
57
|
+
if 'N_OF_DIM' in out.keys():
|
|
58
|
+
out['NDIM'] = int(out['N_OF_DIM'])
|
|
59
|
+
if 'NPIX' in out.keys():
|
|
60
|
+
out['NPIX'] = int(out['NPIX'])
|
|
61
|
+
if 'LOGW' in out.keys():
|
|
62
|
+
out['LOGW'] = int(out['LOGW'])
|
|
63
|
+
if 'VACUUM' in out.keys():
|
|
64
|
+
out['VACUUM'] = int(out['VACUUM'])
|
|
65
|
+
if 'N_P' in out.keys():
|
|
66
|
+
n_p = [int(n) for n in out['N_P']]
|
|
67
|
+
out['N_P'] = n_p
|
|
68
|
+
# Get wave
|
|
69
|
+
npix = out['NPIX']
|
|
70
|
+
w0 = out['WAVE'][0]
|
|
71
|
+
dw = out['WAVE'][1]
|
|
72
|
+
wave = np.arange(npix)*dw+w0
|
|
73
|
+
if out['LOGW']==1:
|
|
74
|
+
wave = 10**wave
|
|
75
|
+
out['WAVELENGTH'] = wave
|
|
76
|
+
# Labels
|
|
77
|
+
labels = []
|
|
78
|
+
for i in range(out['NDIM']):
|
|
79
|
+
label1 = out['LABEL('+str(i+1)+')']
|
|
80
|
+
labels.append(label1)
|
|
81
|
+
out['LABELS'] = labels
|
|
82
|
+
# Array for each label
|
|
83
|
+
for i in range(out['NDIM']):
|
|
84
|
+
name = labels[i]
|
|
85
|
+
llim = out['LLIMITS'][i]
|
|
86
|
+
step = out['STEPS'][i]
|
|
87
|
+
n_p = out['N_P'][i]
|
|
88
|
+
vals = np.arange(n_p)*step+llim
|
|
89
|
+
out[name] = vals
|
|
90
|
+
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def interp(pars,wave=None,cont=None,ncont=None,grid='jwstgiant4.dat',
|
|
95
|
+
griddir=None,ferresrc=None):
|
|
96
|
+
"""
|
|
97
|
+
Interpolate in the FERRE grid.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
pars : numpy array
|
|
102
|
+
Array of parameters/labels. This can be a 2D array [Nstars,Nlabels].
|
|
103
|
+
wave : numpy array, optional
|
|
104
|
+
Output wavelengths. By default, all wavelengths of the grid are used.
|
|
105
|
+
cont : int, optional
|
|
106
|
+
FERRE "cont" continuum parameter.
|
|
107
|
+
ncont : int, optional
|
|
108
|
+
FERRE "ncont" continuum parameter.
|
|
109
|
+
grid : str, optional
|
|
110
|
+
The FERRE grid name.
|
|
111
|
+
griddir : str, optional
|
|
112
|
+
Directory of the FERRE grids.
|
|
113
|
+
ferresrc : str, optional
|
|
114
|
+
Filename of FERRE executable.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
out : dict
|
|
119
|
+
Output parameters. Dictionary with 'wave' and 'flux'. If a 2D parameter
|
|
120
|
+
is input, then 'flux' will be 2D but 'wave' will still be 1D.
|
|
121
|
+
|
|
122
|
+
Example
|
|
123
|
+
-------
|
|
124
|
+
|
|
125
|
+
out = interp([5000.0, 2.5, -1.2, 0.2])
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
if griddir is None:
|
|
130
|
+
griddir = '/Users/nidever/synspec/winter2017/jwst/'
|
|
131
|
+
gridfile = griddir+grid
|
|
132
|
+
if ferresrc is None:
|
|
133
|
+
ferresrc = '/Users/nidever/projects/ferre/bin/ferre.x'
|
|
134
|
+
info = gridinfo(gridfile)
|
|
135
|
+
|
|
136
|
+
# Number of objects
|
|
137
|
+
if np.array(pars).ndim==2:
|
|
138
|
+
nobj = pars.shape[0]
|
|
139
|
+
else:
|
|
140
|
+
nobj = 1
|
|
141
|
+
|
|
142
|
+
# Read the header information
|
|
143
|
+
f = open(gridfile,'r')
|
|
144
|
+
header = []
|
|
145
|
+
line = f.readline().strip()
|
|
146
|
+
while line != '/':
|
|
147
|
+
header.append(line)
|
|
148
|
+
line = f.readline().strip()
|
|
149
|
+
f.close()
|
|
150
|
+
|
|
151
|
+
# Set up temporary directory
|
|
152
|
+
tmpdir = tempfile.mkdtemp(prefix='ferre')
|
|
153
|
+
curdir = os.path.abspath(os.curdir)
|
|
154
|
+
os.chdir(tmpdir)
|
|
155
|
+
|
|
156
|
+
# Create fitting input file
|
|
157
|
+
gridbase = os.path.basename(gridfile)
|
|
158
|
+
os.symlink(gridfile,tmpdir+'/'+gridbase)
|
|
159
|
+
os.symlink(gridfile.replace('.dat','.unf'),tmpdir+'/'+gridbase.replace('.dat','.unf'))
|
|
160
|
+
os.symlink(gridfile.replace('.dat','.hdr'),tmpdir+'/'+gridbase.replace('.dat','.hdr'))
|
|
161
|
+
lines = []
|
|
162
|
+
lines += ["&LISTA"]
|
|
163
|
+
lines += ["NDIM = 4"]
|
|
164
|
+
lines += ["NOV = 0"]
|
|
165
|
+
lines += ["INDV = 1 2 3 4"]
|
|
166
|
+
lines += ["SYNTHFILE(1) = '"+gridbase+"'"]
|
|
167
|
+
lines += ["F_FORMAT = 1"]
|
|
168
|
+
lines += ["PFILE = 'ferre.ipf'"]
|
|
169
|
+
lines += ["FFILE = 'ferre.frd'"]
|
|
170
|
+
lines += ["OFFILE = 'ferre.mdl'"] # output best-fit models
|
|
171
|
+
if wave is not None:
|
|
172
|
+
lines += ["WFILE = 'ferre.wav'"]
|
|
173
|
+
lines += ["WINTER = 2"] # wavelength interpolate the model fluxes
|
|
174
|
+
lines += ["NOBJ = "+str(nobj)]
|
|
175
|
+
if cont is not None:
|
|
176
|
+
lines += ["CONT = "+str(cont)] # Running mean normalization
|
|
177
|
+
lines += ["NCONT = "+str(ncont)] # Npixel for running mean
|
|
178
|
+
lines += ["ERRBAR = 1"]
|
|
179
|
+
lines += ["/"]
|
|
180
|
+
dln.writelines('input.nml',lines)
|
|
181
|
+
|
|
182
|
+
# Write the .wav file
|
|
183
|
+
if wave is not None:
|
|
184
|
+
wlines = ''.join(['{:14.5E}'.format(w) for w in wave])
|
|
185
|
+
dln.writelines('ferre.wav',wlines)
|
|
186
|
+
|
|
187
|
+
# Write the IPF file
|
|
188
|
+
if np.array(pars).ndim==2:
|
|
189
|
+
flines = []
|
|
190
|
+
for i in range(nobj):
|
|
191
|
+
flines += ['test{:d} {:.3f} {:.3f} {:.3f} {:.3f}'.format(i+1,*pars[i,:])]
|
|
192
|
+
else:
|
|
193
|
+
nstars = 1
|
|
194
|
+
flines = 'test1 {:.3f} {:.3f} {:.3f} {:.3f}'.format(*pars)
|
|
195
|
+
dln.writelines('ferre.ipf',flines)
|
|
196
|
+
|
|
197
|
+
# Run FERRE
|
|
198
|
+
if os.path.exists('ferre.mdl'): os.remove('ferre.mdl')
|
|
199
|
+
try:
|
|
200
|
+
result = subprocess.check_output([ferresrc],stderr=subprocess.STDOUT)
|
|
201
|
+
except:
|
|
202
|
+
#traceback.print_exc()
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# Read the output
|
|
206
|
+
mlines = dln.readlines('ferre.mdl')
|
|
207
|
+
if nobj==1:
|
|
208
|
+
mlines = mlines[0]
|
|
209
|
+
mflux = np.array(mlines.split()).astype(float)
|
|
210
|
+
else:
|
|
211
|
+
npix = len(np.array(mlines[0].split()).astype(float))
|
|
212
|
+
mflux = np.zeros((nobj,npix),float)
|
|
213
|
+
for i in range(nobj):
|
|
214
|
+
mflux[i,:] = np.array(mlines[i].split()).astype(float)
|
|
215
|
+
|
|
216
|
+
# Load the wavelengths
|
|
217
|
+
if wave is None:
|
|
218
|
+
wave = info['WAVELENGTH'].copy()
|
|
219
|
+
|
|
220
|
+
out = {'wave':wave,'flux':mflux}
|
|
221
|
+
|
|
222
|
+
# Delete temporary files and directory
|
|
223
|
+
os.chdir(curdir)
|
|
224
|
+
shutil.rmtree(tmpdir)
|
|
225
|
+
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class FERRE(object):
|
|
230
|
+
|
|
231
|
+
def __init__(self,grid='jwstgiant4.dat',outwave=None,cnorder=6,cperclevel=90.0,cbinsize=0.1,
|
|
232
|
+
loggrelation=False,verbose=False):
|
|
233
|
+
"""
|
|
234
|
+
outwave: output wavelength array. By default, the full grid wavelength array will be used.
|
|
235
|
+
cnorder: continuum normalization polynomial order Default is 6.
|
|
236
|
+
cperclevel: continuum normalization percentile level for bins. Default is 90.
|
|
237
|
+
cbinsize: continuum normalization fractional wavelength range (from -1 to 1) to bin. Default is 0.1.
|
|
238
|
+
"""
|
|
239
|
+
gridfile = '/Users/nidever/synspec/winter2017/jwst/'+grid
|
|
240
|
+
ferre = '/Users/nidever/projects/ferre/bin/ferre.x'
|
|
241
|
+
info = gridinfo(gridfile)
|
|
242
|
+
self.grid = grid
|
|
243
|
+
self.gridfile = gridfile
|
|
244
|
+
self.ferre = ferre
|
|
245
|
+
self.info = info
|
|
246
|
+
self.labels = info['LABELS']
|
|
247
|
+
self.nlabels = len(self.labels)
|
|
248
|
+
self.npix = self.info['NPIX']
|
|
249
|
+
self.ranges = np.zeros((self.nlabels,2),float)
|
|
250
|
+
for i in range(self.nlabels):
|
|
251
|
+
self.ranges[i,0] = np.min(self.info[self.labels[i]])
|
|
252
|
+
self.ranges[i,1] = np.max(self.info[self.labels[i]])
|
|
253
|
+
self.steps = self.info['STEPS']
|
|
254
|
+
self.npoints = self.info['N_P']
|
|
255
|
+
self.nspectra = np.sum(self.npoints)
|
|
256
|
+
self.vacuum = self.info['VACUUM']
|
|
257
|
+
self.resolution = self.info['RESOLUTION']
|
|
258
|
+
if outwave is not None:
|
|
259
|
+
self.outwave = outwave
|
|
260
|
+
else:
|
|
261
|
+
self.outwave = None
|
|
262
|
+
self.cnorder = cnorder
|
|
263
|
+
self.cperclevel = cperclevel
|
|
264
|
+
self.cbinsize = cbinsize
|
|
265
|
+
self.loggrelation = loggrelation
|
|
266
|
+
self.verbose = verbose
|
|
267
|
+
self.ncall = 0
|
|
268
|
+
self.njac = 0
|
|
269
|
+
|
|
270
|
+
# Use logg relation
|
|
271
|
+
if loggrelation:
|
|
272
|
+
# Get logg label
|
|
273
|
+
loggind, = np.where(np.char.array(self.labels).lower()=='logg')
|
|
274
|
+
if len(loggind)==0:
|
|
275
|
+
raise ValueError('No logg label')
|
|
276
|
+
self.loggind = loggind[0]
|
|
277
|
+
# Get temperature label
|
|
278
|
+
teffind, = np.where(np.char.array(self.labels).lower()=='teff')
|
|
279
|
+
if len(teffind)==0:
|
|
280
|
+
teffind, = np.where(np.char.array(self.labels).lower().find('temp')>-1)
|
|
281
|
+
self.teffind = teffind[0]
|
|
282
|
+
# Get metallicity label
|
|
283
|
+
fehind, = np.where(np.char.array(self.labels).lower()=='feh')
|
|
284
|
+
if len(fehind)==0:
|
|
285
|
+
fehind, = np.where(np.char.array(self.labels).lower()=='metal')
|
|
286
|
+
if len(fehind)==0:
|
|
287
|
+
fehind, = np.where(np.char.array(self.labels).lower()=='mh')
|
|
288
|
+
if len(fehind)==0:
|
|
289
|
+
fehind, = np.where(np.char.array(self.labels).lower()=='[fe/h]')
|
|
290
|
+
if len(fehind)==0:
|
|
291
|
+
fehind, = np.where(np.char.array(self.labels).lower()=='[m/h]')
|
|
292
|
+
if len(fehind)==0:
|
|
293
|
+
raise ValueError('No metallicity label')
|
|
294
|
+
self.fehind = fehind[0]
|
|
295
|
+
# Get alpha abundance label
|
|
296
|
+
alphaind, = np.where(np.char.array(self.labels).lower()=='[alpha/fe]')
|
|
297
|
+
if len(alphaind)==0:
|
|
298
|
+
alphaind, = np.where(np.char.array(self.labels).lower()=='alphafe')
|
|
299
|
+
if len(alphaind)==0:
|
|
300
|
+
alphaind, = np.where(np.char.array(self.labels).lower()=='alpha_fe')
|
|
301
|
+
if len(alphaind)==0:
|
|
302
|
+
alphaind, = np.where(np.char.array(self.labels).lower().find('alpha')>-1)
|
|
303
|
+
if len(alphaind)==0:
|
|
304
|
+
self.alphaind = None
|
|
305
|
+
else:
|
|
306
|
+
self.alphaind = alphaind[0]
|
|
307
|
+
# Load the ANN model
|
|
308
|
+
logg_model = emulator.Emulator.load(utils.datadir()+'apogeedr17_rgb_logg_ann.npz')
|
|
309
|
+
self.logg_model = logg_model
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def __repr__(self):
|
|
313
|
+
""" String representation."""
|
|
314
|
+
out = self.__class__.__name__+'({:s}, {:d} labels, {:d} spectra, {:d} pixels)\n'.format(self.grid,self.nlabels,
|
|
315
|
+
self.nspectra,self.npix)
|
|
316
|
+
for i in range(self.nlabels):
|
|
317
|
+
out += '{:8.2f} <= {:^12s} <= {:8.2f}, step {:8.2f} [{:d}]\n'.format(self.ranges[i,0],self.labels[i],
|
|
318
|
+
self.ranges[i,1],self.steps[i],self.npoints[i])
|
|
319
|
+
out += '{:8.2f} <= {:^12s} <= {:8.2f}, step {:8.2f} [{:d}]\n'.format(np.min(self.info['WAVELENGTH']),
|
|
320
|
+
'WAVE',np.max(self.info['WAVELENGTH']),
|
|
321
|
+
self.info['WAVE'][1],self.npix)
|
|
322
|
+
return out
|
|
323
|
+
|
|
324
|
+
def __call__(self,pars,wave=None,cnorder=None,cperclevel=None,cbinsize=None,norm=True):
|
|
325
|
+
"""
|
|
326
|
+
pars: array of parameters. Can be 2D [Nstars,Nlabels].
|
|
327
|
+
"""
|
|
328
|
+
if wave is None:
|
|
329
|
+
wave = self.outwave
|
|
330
|
+
if cnorder is None:
|
|
331
|
+
cnorder = self.cnorder
|
|
332
|
+
if cperclevel is None:
|
|
333
|
+
cperclevel = self.cperclevel
|
|
334
|
+
if cbinsize is None:
|
|
335
|
+
cbinsize = self.cbinsize
|
|
336
|
+
|
|
337
|
+
pars = np.array(pars)
|
|
338
|
+
if pars.ndim==2:
|
|
339
|
+
ninputlabels = pars.shape[1]
|
|
340
|
+
else:
|
|
341
|
+
ninputlabels = len(pars)
|
|
342
|
+
if (ninputlabels != self.nlabels and self.loggrelation==False) or \
|
|
343
|
+
(ninputlabels != (self.nlabels-1) and self.loggrelation==True):
|
|
344
|
+
raise ValueError('Number of labels not correct.')
|
|
345
|
+
|
|
346
|
+
# Use the logg relation (as a function of Teff/feh/alpha)
|
|
347
|
+
if self.loggrelation:
|
|
348
|
+
# Multiple parameters
|
|
349
|
+
if pars.ndim==2:
|
|
350
|
+
nobj = np.array(pars).shape[0]
|
|
351
|
+
newpars = np.zeros((nobj,self.nlabels),float)
|
|
352
|
+
for i in range(nobj):
|
|
353
|
+
newpars[i,:] = self.getlogg(pars[i,:])
|
|
354
|
+
# Single set of labels
|
|
355
|
+
else:
|
|
356
|
+
nobj = 1
|
|
357
|
+
newpars = self.getlogg(pars)
|
|
358
|
+
else:
|
|
359
|
+
# Multiple parameters
|
|
360
|
+
if pars.ndim==2:
|
|
361
|
+
nobj = np.array(pars).shape[0]
|
|
362
|
+
else:
|
|
363
|
+
nobj = 1
|
|
364
|
+
newpars = pars
|
|
365
|
+
|
|
366
|
+
self.ncall += 1
|
|
367
|
+
out = interp(newpars,wave=wave)
|
|
368
|
+
|
|
369
|
+
# Now do the continuum normalization
|
|
370
|
+
if cnorder is not None and norm==True:
|
|
371
|
+
wave = out['wave']
|
|
372
|
+
flux = out['flux'].copy()
|
|
373
|
+
if nobj==1:
|
|
374
|
+
out['flux'] = self.normalize(wave,flux)
|
|
375
|
+
else:
|
|
376
|
+
for i in range(nobj):
|
|
377
|
+
out['flux'][i,:] = self.normalize(wave,flux[i,:])
|
|
378
|
+
|
|
379
|
+
return out
|
|
380
|
+
|
|
381
|
+
def getlogg(self,pars):
|
|
382
|
+
""" Get logg from the logg relation and fill it into the parameter array where it belongs."""
|
|
383
|
+
# The only parameters should be input, with the logg one missing/excluded
|
|
384
|
+
# Insert a dummy value for logg
|
|
385
|
+
newpars = np.insert(pars,self.loggind,0.0)
|
|
386
|
+
teff = newpars[self.teffind]
|
|
387
|
+
feh = newpars[self.fehind]
|
|
388
|
+
if self.alphaind is not None:
|
|
389
|
+
alpha = newpars[self.alphaind]
|
|
390
|
+
else:
|
|
391
|
+
alpha = 0.0
|
|
392
|
+
logg = self.logg_model([teff,feh,alpha],border='extrapolate')
|
|
393
|
+
newpars[self.loggind] = logg
|
|
394
|
+
return newpars
|
|
395
|
+
|
|
396
|
+
def normalize(self,wave,flux,norder=None,perclevel=None,binsize=None):
|
|
397
|
+
""" Normalize a spectrum."""
|
|
398
|
+
if norder is None:
|
|
399
|
+
norder = self.cnorder
|
|
400
|
+
if perclevel is None:
|
|
401
|
+
perclevel = self.cperclevel
|
|
402
|
+
if binsize is None:
|
|
403
|
+
binsize = self.cbinsize
|
|
404
|
+
sp = Spec1D(flux,wave=wave,err=flux*0)
|
|
405
|
+
cont = continuum(sp,norder=norder,perclevel=perclevel,binsize=binsize)
|
|
406
|
+
newflux = flux/cont
|
|
407
|
+
return newflux
|
|
408
|
+
|
|
409
|
+
def model(self,wave,*pars,**kwargs):
|
|
410
|
+
""" Model function for curve_fit."""
|
|
411
|
+
if self.verbose:
|
|
412
|
+
print('model: ',pars)
|
|
413
|
+
out = self(pars,wave=wave,**kwargs)
|
|
414
|
+
# Only return the flux
|
|
415
|
+
return out['flux']
|
|
416
|
+
|
|
417
|
+
def jac(self,wave,*args,retmodel=False,allpars=False,**kwargs):
|
|
418
|
+
"""
|
|
419
|
+
Method to return Jacobian matrix.
|
|
420
|
+
This includes the contribution of the lookup table.
|
|
421
|
+
|
|
422
|
+
Parameters
|
|
423
|
+
----------
|
|
424
|
+
wave : numpy array
|
|
425
|
+
Wavelength array.
|
|
426
|
+
args : float
|
|
427
|
+
Model parameter values as separate positional input parameters,
|
|
428
|
+
[amp, xcen, ycen, sky]. If allpars=True, then the model
|
|
429
|
+
parameters are added at the end, i.e.
|
|
430
|
+
[amp, xcen, ycen, sky, model parameters].
|
|
431
|
+
retmodel : boolean, optional
|
|
432
|
+
Return the model as well. Default is retmodel=False.
|
|
433
|
+
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
if retmodel==False
|
|
437
|
+
jac : numpy array
|
|
438
|
+
Jacobian matrix of partial derivatives [N,Npars].
|
|
439
|
+
model : numpy array
|
|
440
|
+
Array of (1-D) model values for the input xdata and parameters.
|
|
441
|
+
If retmodel==True, then (model,jac) are returned.
|
|
442
|
+
|
|
443
|
+
Example
|
|
444
|
+
-------
|
|
445
|
+
|
|
446
|
+
jac = ferre.jac(xdata,*pars)
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
# logg relation
|
|
450
|
+
# add a dummy logg value in
|
|
451
|
+
if self.loggrelation:
|
|
452
|
+
fullargs = np.insert(args,self.loggind,0.0)
|
|
453
|
+
else:
|
|
454
|
+
fullargs = args
|
|
455
|
+
|
|
456
|
+
if self.verbose:
|
|
457
|
+
print('jac: ',args)
|
|
458
|
+
|
|
459
|
+
# Loop over parameters
|
|
460
|
+
pars = [np.array(copy.deepcopy(args))]
|
|
461
|
+
steps = []
|
|
462
|
+
for i in range(self.nlabels):
|
|
463
|
+
if self.loggrelation and i==self.loggind:
|
|
464
|
+
continue
|
|
465
|
+
targs = np.array(copy.deepcopy(fullargs))
|
|
466
|
+
step = 0.1*self.steps[i]
|
|
467
|
+
# Check boundaries, if above upper boundary
|
|
468
|
+
# go the opposite way
|
|
469
|
+
if targs[i]>self.ranges[i,1]:
|
|
470
|
+
step *= -1
|
|
471
|
+
targs[i] += step
|
|
472
|
+
# Remove dummy logg if using logg relation
|
|
473
|
+
if self.loggrelation:
|
|
474
|
+
targs = np.delete(targs,self.loggind)
|
|
475
|
+
pars.append(targs)
|
|
476
|
+
steps.append(step)
|
|
477
|
+
|
|
478
|
+
# Reformat pars into arrays
|
|
479
|
+
pars = np.array(pars)
|
|
480
|
+
steps = np.array(steps)
|
|
481
|
+
|
|
482
|
+
# Run FERRE
|
|
483
|
+
out = self(pars,wave=wave)
|
|
484
|
+
flux = out['flux']
|
|
485
|
+
npix = flux.shape[1]
|
|
486
|
+
|
|
487
|
+
# Initialize jacobian matrix
|
|
488
|
+
fjac = np.zeros((npix,len(steps)),np.float64)
|
|
489
|
+
f0 = flux[0,:]
|
|
490
|
+
for i in range(len(steps)):
|
|
491
|
+
f1 = flux[i+1,:]
|
|
492
|
+
fjac[:,i] = (f1-f0)/steps[i]
|
|
493
|
+
|
|
494
|
+
self.njac += 1
|
|
495
|
+
|
|
496
|
+
return fjac
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def fit(slist,inter=3,algor=1,init=1,indini=None,nruns=1,
|
|
500
|
+
cont=1,ncont=0,errbar=1,grid='jwstgiant4.dat',save=False):
|
|
501
|
+
"""
|
|
502
|
+
Run FERRE on list of spectra
|
|
503
|
+
|
|
504
|
+
Parameters
|
|
505
|
+
----------
|
|
506
|
+
slist : list
|
|
507
|
+
A list of dictionaries with wave, flux, err and vrel.
|
|
508
|
+
vrel : list
|
|
509
|
+
Input list of doppler shifts for the spectra.
|
|
510
|
+
inter : int, optional
|
|
511
|
+
Interpolation algorithm.
|
|
512
|
+
0 nearest neighbor
|
|
513
|
+
1 linear
|
|
514
|
+
2 quadratic Bezier
|
|
515
|
+
3 cubic Bezier
|
|
516
|
+
4 cubic spline
|
|
517
|
+
Default is 3.
|
|
518
|
+
algor : int, optional
|
|
519
|
+
Search algorithm?
|
|
520
|
+
-1 weighted average over the grid likely
|
|
521
|
+
0 pixel with lowest value minlocus
|
|
522
|
+
1 Nelder-mead method (Miller's) minim
|
|
523
|
+
2 BTR method (Csendes/Miller) global
|
|
524
|
+
3 uobyqa method (Powell/Miller) uobyqa
|
|
525
|
+
4 truncated Newton lmqnbc
|
|
526
|
+
5 MCMC mcmcde
|
|
527
|
+
Default is 1.
|
|
528
|
+
init : int, optional
|
|
529
|
+
Starting point for search.
|
|
530
|
+
0 use the values in pfile
|
|
531
|
+
1 follow the rules set by keyword indini
|
|
532
|
+
Default is 1.
|
|
533
|
+
indini : list, optional
|
|
534
|
+
Individual control of starting points for each parameter.
|
|
535
|
+
nruns : int, optional
|
|
536
|
+
Number of searches to be done. Default is 1.
|
|
537
|
+
cont : int, optional
|
|
538
|
+
Continuum parameter:
|
|
539
|
+
1 polynomial fitting (ncont order, 0-constant, 1-linear)
|
|
540
|
+
2 segmented normaleization (ncont segments)
|
|
541
|
+
3 running mean (ncont pixels)
|
|
542
|
+
ncont : int, optional
|
|
543
|
+
Number for continuum normalization. If cont=1, then ncont
|
|
544
|
+
gives the polynomial order. If cont=2, then ncont is the
|
|
545
|
+
number of segements. If cont=3, then ncont is the number
|
|
546
|
+
of pixels for the running mean.
|
|
547
|
+
errbar : int, optional
|
|
548
|
+
Choice of algorithm to compute error bars.
|
|
549
|
+
0 adopt the distance from solution at which chisq=min(chisq)+1
|
|
550
|
+
1 invert the curvature matrix
|
|
551
|
+
Default is 1.
|
|
552
|
+
save : bool, optional
|
|
553
|
+
Do not delete the temporary directory. Default is save=False.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
tab : table
|
|
558
|
+
Output table of values.
|
|
559
|
+
info : list
|
|
560
|
+
List of dictionaries, one per spectrum, that includes spectra and arrays.
|
|
561
|
+
|
|
562
|
+
Example
|
|
563
|
+
-------
|
|
564
|
+
|
|
565
|
+
tab,info = fit(files,vrel,algor=1,nruns=5)
|
|
566
|
+
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
gridfile = '/Users/nidever/synspec/winter2017/jwst/'+grid
|
|
570
|
+
ferre = '/Users/nidever/projects/ferre/bin/ferre.x'
|
|
571
|
+
|
|
572
|
+
# Set up temporary directory
|
|
573
|
+
tmpdir = tempfile.mkdtemp(prefix='ferre')
|
|
574
|
+
curdir = os.path.abspath(os.curdir)
|
|
575
|
+
os.chdir(tmpdir)
|
|
576
|
+
print('Running FERRE in temporary directory '+tmpdir)
|
|
577
|
+
|
|
578
|
+
# Parameters
|
|
579
|
+
print('INTER = ',inter)
|
|
580
|
+
print('ALGOR = ',algor)
|
|
581
|
+
print('INIT = ',init)
|
|
582
|
+
print('INDINI = ',indini)
|
|
583
|
+
print('NRUNS = ',nruns)
|
|
584
|
+
print('CONT = ',cont)
|
|
585
|
+
print('NCONT = ',ncont)
|
|
586
|
+
print('ERRBAR = ',errbar)
|
|
587
|
+
|
|
588
|
+
# Create fitting input file
|
|
589
|
+
gridbase = os.path.basename(gridfile)
|
|
590
|
+
os.symlink(gridfile,tmpdir+'/'+gridbase)
|
|
591
|
+
os.symlink(gridfile.replace('.dat','.unf'),tmpdir+'/'+gridbase.replace('.dat','.unf'))
|
|
592
|
+
os.symlink(gridfile.replace('.dat','.hdr'),tmpdir+'/'+gridbase.replace('.dat','.hdr'))
|
|
593
|
+
lines = []
|
|
594
|
+
lines += ["&LISTA"]
|
|
595
|
+
lines += ["NDIM = 4"]
|
|
596
|
+
lines += ["NOV = 4"]
|
|
597
|
+
lines += ["INDV = 1 2 3 4"]
|
|
598
|
+
lines += ["SYNTHFILE(1) = '"+gridbase+"'"]
|
|
599
|
+
lines += ["F_FORMAT = 1"]
|
|
600
|
+
lines += ["INTER = "+str(inter)] # cubic Bezier interpolation
|
|
601
|
+
lines += ["ALGOR = "+str(algor)] # 1-Nelder-Mead,5-MCMC
|
|
602
|
+
lines += ["INIT = "+str(init)]
|
|
603
|
+
if indini is not None:
|
|
604
|
+
lines += ['INDINI = '+np.array2string(np.array(indini)).strip('[]')]
|
|
605
|
+
nruns = 1
|
|
606
|
+
for term in indini:
|
|
607
|
+
nruns = nruns * term
|
|
608
|
+
# lines += ["INDINI = "+str(indini)]
|
|
609
|
+
if nruns is not None:
|
|
610
|
+
lines += ["NRUNS = "+str(nruns)]
|
|
611
|
+
lines += ["PFILE = 'ferre.ipf'"]
|
|
612
|
+
lines += ["FFILE = 'ferre.frd'"]
|
|
613
|
+
lines += ["ERFILE = 'ferre.err'"]
|
|
614
|
+
lines += ["WFILE = 'ferre.wav'"]
|
|
615
|
+
lines += ["OPFILE = 'ferre.opf'"] # output best-fit parameters and uncertainties
|
|
616
|
+
lines += ["OFFILE = 'ferre.mdl'"] # output best-fit models
|
|
617
|
+
lines += ["SFFILE = 'ferre.nrd'"] # normalized data
|
|
618
|
+
lines += ["NOBJ = "+str(len(slist))]
|
|
619
|
+
lines += ["CONT = "+str(cont)] # Running mean normalization
|
|
620
|
+
lines += ["NCONT = "+str(ncont)] # Npixel for running mean
|
|
621
|
+
lines += ["WINTER = 2"] # wavelength interpolate the model fluxes
|
|
622
|
+
lines += ["ERRBAR = "+str(errbar)]
|
|
623
|
+
lines += ["/"]
|
|
624
|
+
dln.writelines('input.nml',lines)
|
|
625
|
+
|
|
626
|
+
# Put star with maximum npix first
|
|
627
|
+
# FERRE uses this to set the maximum pixels for ALL the spectra
|
|
628
|
+
for i in range(len(slist)):
|
|
629
|
+
if 'npix' not in slist[i].keys():
|
|
630
|
+
slist[i]['npix'] = len(slist[i]['flux'])
|
|
631
|
+
if 'id' not in slist[i].keys():
|
|
632
|
+
slist[i]['id'] = i+1
|
|
633
|
+
npixall = [s['npix'] for s in slist]
|
|
634
|
+
maxind = np.argmax(npixall)
|
|
635
|
+
npix = np.max(npixall)
|
|
636
|
+
if maxind != 0:
|
|
637
|
+
slist1 = slist.pop(maxind)
|
|
638
|
+
slist = [slist1] + slist
|
|
639
|
+
|
|
640
|
+
# Make the list of lines to write out for FERRE
|
|
641
|
+
flines,elines,wlines,plines = [],[],[],[]
|
|
642
|
+
for i in range(len(slist)):
|
|
643
|
+
# Need to buffer the arrays to have consistent number of pixels
|
|
644
|
+
flux = slist[i]['flux']
|
|
645
|
+
err = slist[i]['err']
|
|
646
|
+
wave = slist[i]['wave']
|
|
647
|
+
if len(flux)<npix:
|
|
648
|
+
nmissing = npix-len(flux)
|
|
649
|
+
flux = np.hstack((flux,np.zeros(nmissing)))
|
|
650
|
+
err = np.hstack((err,np.zeros(nmissing)+1e10))
|
|
651
|
+
wave = np.hstack((wave,np.zeros(nmissing)))
|
|
652
|
+
|
|
653
|
+
# Convert to ASCII for FERRE
|
|
654
|
+
obs = ''.join(['{:14.5E}'.format(f) for f in flux])
|
|
655
|
+
obserr = ''.join(['{:14.5E}'.format(e) for e in err])
|
|
656
|
+
obswave = ''.join(['{:14.5E}'.format(w) for w in wave])
|
|
657
|
+
slist[i]['ferre_fline'] = obs
|
|
658
|
+
slist[i]['ferre_eline'] = obserr
|
|
659
|
+
slist[i]['ferre_wline'] = obswave
|
|
660
|
+
slist[i]['ferre_pline'] = 'spec'+str(i+1)+' 4000.0 2.5 -0.5 0.1'
|
|
661
|
+
slist[i]['ferre_id'] = 'spec'+str(i+1)
|
|
662
|
+
|
|
663
|
+
flines.append(slist[i]['ferre_fline'])
|
|
664
|
+
elines.append(slist[i]['ferre_eline'])
|
|
665
|
+
wlines.append(slist[i]['ferre_wline'])
|
|
666
|
+
plines.append(slist[i]['ferre_pline'])
|
|
667
|
+
|
|
668
|
+
dln.writelines('ferre.frd',flines)
|
|
669
|
+
dln.writelines('ferre.err',elines)
|
|
670
|
+
dln.writelines('ferre.wav',wlines)
|
|
671
|
+
dln.writelines('ferre.ipf',plines)
|
|
672
|
+
|
|
673
|
+
# Run FERRE
|
|
674
|
+
if os.path.exists('ferre.mdl'): os.remove('ferre.mdl')
|
|
675
|
+
if os.path.exists('ferre.opf'): os.remove('ferre.opf')
|
|
676
|
+
print('Running FERRE on '+str(len(slist))+' spectra')
|
|
677
|
+
try:
|
|
678
|
+
fout = open('ferre.log','w')
|
|
679
|
+
out = subprocess.call([ferre],stdout=fout,stderr=subprocess.STDOUT)
|
|
680
|
+
fout.close()
|
|
681
|
+
loglines = dln.readlines('ferre.log')
|
|
682
|
+
#out = subprocess.check_output([ferre])
|
|
683
|
+
except:
|
|
684
|
+
fout.close()
|
|
685
|
+
#traceback.print_exc()
|
|
686
|
+
pass
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# Read the output
|
|
690
|
+
olines = dln.readlines('ferre.opf')
|
|
691
|
+
ids = np.char.array([o.split()[0] for o in olines])
|
|
692
|
+
mlines = dln.readlines('ferre.mdl')
|
|
693
|
+
slines = dln.readlines('ferre.nrd')
|
|
694
|
+
nolines = len(olines)
|
|
695
|
+
print(str(nolines)+' lines in output files')
|
|
696
|
+
|
|
697
|
+
# Loop over slist
|
|
698
|
+
for i in range(len(slist)):
|
|
699
|
+
slist1 = slist[i]
|
|
700
|
+
filename = slist1['filename']
|
|
701
|
+
ferre_id = slist1['ferre_id']
|
|
702
|
+
|
|
703
|
+
# Find the right output line for this star
|
|
704
|
+
ind, = np.where(ids==ferre_id)
|
|
705
|
+
if len(ind)==0:
|
|
706
|
+
print(ferre_id+' not found in output file')
|
|
707
|
+
slist1['success'] = False
|
|
708
|
+
slist1['pars'] = None
|
|
709
|
+
slist1['parerr'] = None
|
|
710
|
+
slist1['snr2'] = None
|
|
711
|
+
slist1['rchisq'] = None
|
|
712
|
+
slist1['model'] = None
|
|
713
|
+
slist1['smflux'] = None
|
|
714
|
+
continue
|
|
715
|
+
ind = ind[0]
|
|
716
|
+
olines1 = olines[ind]
|
|
717
|
+
mlines1 = mlines[ind]
|
|
718
|
+
slines1 = slines[ind]
|
|
719
|
+
|
|
720
|
+
# opfile has name, pars, parerr, fraction of phot data points, log(S/N)^2, log(reduced chisq)
|
|
721
|
+
arr = olines1.split()
|
|
722
|
+
pars = np.array(arr[1:5]).astype(float)
|
|
723
|
+
parerr = np.array(arr[5:9]).astype(float)
|
|
724
|
+
logsnr2 = float(arr[10])
|
|
725
|
+
snr2 = 10**logsnr2
|
|
726
|
+
try:
|
|
727
|
+
logrchisq = float(arr[11])
|
|
728
|
+
rchisq = 10**logrchisq
|
|
729
|
+
except:
|
|
730
|
+
logrchisq = np.nan
|
|
731
|
+
rchisq = np.nan
|
|
732
|
+
slist1['success'] = True
|
|
733
|
+
slist1['pars'] = pars
|
|
734
|
+
slist1['parerr'] = parerr
|
|
735
|
+
slist1['snr2'] = snr2
|
|
736
|
+
slist1['rchisq'] = rchisq
|
|
737
|
+
|
|
738
|
+
mflux = np.array(mlines1.split()).astype(float)
|
|
739
|
+
smflux = np.array(slines1.split()).astype(float)
|
|
740
|
+
# If this star has less pixels than the first star, then we need to trim some pixels
|
|
741
|
+
# off the end
|
|
742
|
+
if len(mflux) != slist1['npix']:
|
|
743
|
+
mflux = mflux[0:slist1['npix']]
|
|
744
|
+
smflux = smflux[0:slist1['npix']]
|
|
745
|
+
slist1['model'] = mflux
|
|
746
|
+
slist1['smflux'] = smflux
|
|
747
|
+
|
|
748
|
+
# Delete temporary files and directory
|
|
749
|
+
if save==False:
|
|
750
|
+
os.chdir(curdir)
|
|
751
|
+
shutil.rmtree(tmpdir)
|
|
752
|
+
|
|
753
|
+
return slist
|
|
754
|
+
|
|
755
|
+
def specprep(spec,vrel=None):
|
|
756
|
+
""" Prepare the spectra for input to FERRE """
|
|
757
|
+
# Synspec spectra are in AIR, JWST spectra are in VACUUM, convert
|
|
758
|
+
spec.wavevac = False
|
|
759
|
+
flux = np.array([])
|
|
760
|
+
err = np.array([])
|
|
761
|
+
wave = np.array([])
|
|
762
|
+
mask = np.array([])
|
|
763
|
+
for sp in spec:
|
|
764
|
+
# trim edge pixels
|
|
765
|
+
logood = np.where(sp.mask==False)[0][0]
|
|
766
|
+
higood = np.where(sp.mask==False)[0][-1]
|
|
767
|
+
slc = slice(logood+30,higood-30)
|
|
768
|
+
flux = np.concatenate((flux,sp.flux[slc]))
|
|
769
|
+
err = np.concatenate((err,sp.err[slc]))
|
|
770
|
+
wave = np.concatenate((wave,sp.wave[slc]))
|
|
771
|
+
mask = np.concatenate((mask,sp.mask[slc]))
|
|
772
|
+
# Remove Doppler shift
|
|
773
|
+
if vrel is None:
|
|
774
|
+
if hasattr(spec,'vrel')==False:
|
|
775
|
+
raise ValueError('No vrel to use')
|
|
776
|
+
vrel = spec.vrel
|
|
777
|
+
wave /= (1+vrel/cspeed)
|
|
778
|
+
# Trim wavelengths that are too large
|
|
779
|
+
gd, = np.where(wave < 17990)
|
|
780
|
+
flux = flux[gd]
|
|
781
|
+
err = err[gd]
|
|
782
|
+
wave = wave[gd]
|
|
783
|
+
mask = mask[gd]
|
|
784
|
+
# Grow bad mask by ~20 pixels
|
|
785
|
+
oldmask = mask.copy()
|
|
786
|
+
mask = np.convolve(mask,np.ones(61),mode='same').astype(bool)
|
|
787
|
+
# Interpolate over masked pixels
|
|
788
|
+
xpix = np.arange(len(flux))
|
|
789
|
+
bad, = np.where((mask==True) | (flux<=0))
|
|
790
|
+
good, = np.where((mask==False) & (flux>0))
|
|
791
|
+
if len(bad)>0:
|
|
792
|
+
smfilt = utils.nanmedfilt(flux[good],201)
|
|
793
|
+
newpix = dln.interp(xpix[good],smfilt,xpix[bad],kind='linear')
|
|
794
|
+
flux[bad] = newpix
|
|
795
|
+
#flux[bad] = 0.0
|
|
796
|
+
err[bad] = 1e10
|
|
797
|
+
|
|
798
|
+
# Put all the information into slist
|
|
799
|
+
out = {}
|
|
800
|
+
out['npix'] = len(flux)
|
|
801
|
+
out['flux'] = flux
|
|
802
|
+
out['err'] = err
|
|
803
|
+
out['wave'] = wave
|
|
804
|
+
return out
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def cfit(slist,vrel,cont=1,ncont=0,loggrelation=False,grid='jwstgiant4.dat',
|
|
808
|
+
initgrid=True,outlier=True,verbose=False):
|
|
809
|
+
"""
|
|
810
|
+
Fit spectrum with curve_fit running FERRE to get the models
|
|
811
|
+
for each set of parameters.
|
|
812
|
+
|
|
813
|
+
Parameters
|
|
814
|
+
----------
|
|
815
|
+
slist : list
|
|
816
|
+
A list of dictionaries with wave, flux, err and vrel.
|
|
817
|
+
vrel : list
|
|
818
|
+
Input list of doppler shifts for the spectra.
|
|
819
|
+
cont : int, optional
|
|
820
|
+
Continuum parameter:
|
|
821
|
+
1 polynomial fitting (ncont order, 0-constant, 1-linear)
|
|
822
|
+
2 segmented normaleization (ncont segments)
|
|
823
|
+
3 running mean (ncont pixels)
|
|
824
|
+
ncont : int, optional
|
|
825
|
+
Number for continuum normalization. If cont=1, then ncont
|
|
826
|
+
gives the polynomial order. If cont=2, then ncont is the
|
|
827
|
+
number of segements. If cont=3, then ncont is the number
|
|
828
|
+
of pixels for the running mean.
|
|
829
|
+
loggrelation : bool, optional
|
|
830
|
+
Use the logg-relation as a function of Teff/feh/alpha.
|
|
831
|
+
|
|
832
|
+
Returns
|
|
833
|
+
-------
|
|
834
|
+
tab : table
|
|
835
|
+
Output table of values.
|
|
836
|
+
info : list
|
|
837
|
+
List of dictionaries, one per spectrum, that includes spectra and arrays.
|
|
838
|
+
|
|
839
|
+
Example
|
|
840
|
+
-------
|
|
841
|
+
|
|
842
|
+
tab,info = cfit(files,vrel)
|
|
843
|
+
|
|
844
|
+
"""
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
## Loop over the files
|
|
848
|
+
#slist = []
|
|
849
|
+
#for i in range(nfiles):
|
|
850
|
+
# filename = files[i]
|
|
851
|
+
# vrel1 = vrel[i]
|
|
852
|
+
# if filename is None:
|
|
853
|
+
# continue
|
|
854
|
+
# if os.path.exists(filename)==False:
|
|
855
|
+
# print(filename+' not found')
|
|
856
|
+
# continue
|
|
857
|
+
# if os.path.getsize(filename)==0:
|
|
858
|
+
# print(filename+' is empty')
|
|
859
|
+
# continue
|
|
860
|
+
# spec = doppler.read(filename)
|
|
861
|
+
# spec.vrel = vrel1
|
|
862
|
+
# #spec.normalize()
|
|
863
|
+
# slist1 = {'filename':filename,'vrel':vrel1,'snr':spec.snr,'spec':spec}
|
|
864
|
+
# slist.append(slist1)
|
|
865
|
+
#
|
|
866
|
+
#if len(slist)==0:
|
|
867
|
+
# print('No spectra to fit')
|
|
868
|
+
# return None,None
|
|
869
|
+
|
|
870
|
+
if type(slist) is not list:
|
|
871
|
+
slist = [slist]
|
|
872
|
+
nspectra = len(slist)
|
|
873
|
+
|
|
874
|
+
# Loop over spectra
|
|
875
|
+
out = []
|
|
876
|
+
for i in range(nspectra):
|
|
877
|
+
if verbose:
|
|
878
|
+
if i>0: print(' ')
|
|
879
|
+
print('Star '+str(i+1))
|
|
880
|
+
print('-------')
|
|
881
|
+
slist1 = slist[i]
|
|
882
|
+
vrel1 = vrel[i]
|
|
883
|
+
spec = slist1['spec']
|
|
884
|
+
spec.vrel = vrel1
|
|
885
|
+
if verbose:
|
|
886
|
+
print('Vrel: {:.2f} km/s'.format(vrel1))
|
|
887
|
+
print('S/N: {:.2f}'.format(spec.snr))
|
|
888
|
+
# Initialize the FERRE object
|
|
889
|
+
fr = FERRE(loggrelation=loggrelation,verbose=(verbose>1))
|
|
890
|
+
# Prepare spectrum for FERRE
|
|
891
|
+
pspec = specprep(spec)
|
|
892
|
+
# Now normalize
|
|
893
|
+
newflux = fr.normalize(pspec['wave'],pspec['flux'])
|
|
894
|
+
cont = pspec['flux']/newflux
|
|
895
|
+
pspec['oflux'] = pspec['flux']
|
|
896
|
+
pspec['oerr'] = pspec['err']
|
|
897
|
+
pspec['flux'] = newflux
|
|
898
|
+
pspec['err'] = pspec['oerr']/cont
|
|
899
|
+
# Normalize
|
|
900
|
+
fr.outwave = pspec['wave']
|
|
901
|
+
if loggrelation:
|
|
902
|
+
estimates = [4500.0,-1.0,0.0]
|
|
903
|
+
bounds = [np.delete(fr.ranges[:,0],1),np.delete(fr.ranges[:,1],1)]
|
|
904
|
+
else:
|
|
905
|
+
estimates = [4500.0,2.5,-1.0,0.0]
|
|
906
|
+
bounds = [fr.ranges[:,0],fr.ranges[:,1]]
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
# Run grid of ~100 points to get first estimate
|
|
910
|
+
if initgrid:
|
|
911
|
+
if loggrelation:
|
|
912
|
+
nsample = 5
|
|
913
|
+
tstep = np.ptp(fr.ranges[0,:])/nsample
|
|
914
|
+
tgrid = np.arange(nsample)*tstep+fr.ranges[0,0]+tstep*0.5
|
|
915
|
+
mstep = np.ptp(fr.ranges[2,:])/nsample
|
|
916
|
+
mgrid = np.arange(nsample)*mstep+fr.ranges[2,0]+mstep*0.5
|
|
917
|
+
astep = np.ptp(fr.ranges[3,:])/nsample
|
|
918
|
+
agrid = np.arange(nsample)*astep+fr.ranges[3,0]+astep*0.5
|
|
919
|
+
tgrid2d,mgrid2d,agrid2d = np.meshgrid(tgrid,mgrid,agrid)
|
|
920
|
+
gridpars = np.vstack((tgrid2d.flatten(),mgrid2d.flatten(),agrid2d.flatten())).T
|
|
921
|
+
else:
|
|
922
|
+
nsample = 4
|
|
923
|
+
tstep = np.ptp(fr.ranges[0,:])/nsample
|
|
924
|
+
tgrid = np.arange(nsample)*tstep+fr.ranges[0,0]+tstep*0.5
|
|
925
|
+
gstep = np.ptp(fr.ranges[1,:])/nsample
|
|
926
|
+
ggrid = np.arange(nsample)*gstep+fr.ranges[1,0]+gstep*0.5
|
|
927
|
+
mstep = np.ptp(fr.ranges[2,:])/nsample
|
|
928
|
+
mgrid = np.arange(nsample)*mstep+fr.ranges[2,0]+mstep*0.5
|
|
929
|
+
astep = np.ptp(fr.ranges[3,:])/nsample
|
|
930
|
+
agrid = np.arange(nsample)*astep+fr.ranges[3,0]+astep*0.5
|
|
931
|
+
tgrid2d,ggrid2d,mgrid2d,agrid2d = np.meshgrid(tgrid,ggrid,mgrid,agrid)
|
|
932
|
+
gridpars = np.vstack((tgrid2d.flatten(),ggrid2d.flatten(),mgrid2d.flatten(),agrid2d.flatten())).T
|
|
933
|
+
if verbose:
|
|
934
|
+
print('Testing an initial grid of '+str(gridpars.shape[0])+' spectra')
|
|
935
|
+
# Run FERRE
|
|
936
|
+
specgrid = fr(gridpars,wave=pspec['wave'])
|
|
937
|
+
chisqarr = np.sum((specgrid['flux']-pspec['flux'])**2/pspec['err']**2,axis=1)/len(pspec['flux'])
|
|
938
|
+
bestind = np.argmin(chisqarr)
|
|
939
|
+
estimates = gridpars[bestind,:]
|
|
940
|
+
if verbose:
|
|
941
|
+
print('Initial estimates: ',estimates)
|
|
942
|
+
|
|
943
|
+
# Run curve_fit
|
|
944
|
+
try:
|
|
945
|
+
pars,pcov = curve_fit(fr.model,pspec['wave'],pspec['flux'],p0=estimates,
|
|
946
|
+
sigma=pspec['err'],bounds=bounds,jac=fr.jac)
|
|
947
|
+
perror = np.sqrt(np.diag(pcov))
|
|
948
|
+
bestmodel = fr.model(pspec['wave'],*pars)
|
|
949
|
+
chisq = np.sum((pspec['flux']-bestmodel)**2/pspec['err']**2)/len(pspec['flux'])
|
|
950
|
+
|
|
951
|
+
# Get full parameters
|
|
952
|
+
if loggrelation:
|
|
953
|
+
fullpars = fr.getlogg(pars)
|
|
954
|
+
fullperror = np.insert(perror,fr.loggind,0.0)
|
|
955
|
+
else:
|
|
956
|
+
fullpars = pars
|
|
957
|
+
fullperror = perror
|
|
958
|
+
|
|
959
|
+
if verbose:
|
|
960
|
+
printvals = [fullpars[0],fullperror[0],fullpars[1],fullperror[1],fullpars[2],fullperror[2],
|
|
961
|
+
fullpars[3],fullperror[3]]
|
|
962
|
+
print('Best parameters: {:f}+/-{:.3g}, {:.3f}+/-{:.3g}, {:.3f}+/-{:.3g}, {:.3f}+/-{:.3g}'.format(*printvals))
|
|
963
|
+
print('Chisq: ',chisq)
|
|
964
|
+
|
|
965
|
+
# Construct the output dictionary
|
|
966
|
+
out1 = {'index':i,'vrel':vrel1,'snr':spec.snr,'pars':fullpars,'perror':fullperror,'wave':pspec['wave'],
|
|
967
|
+
'flux':pspec['flux'],'err':pspec['err'],'model':bestmodel,'chisq':chisq,
|
|
968
|
+
'loggrelation':loggrelation,'success':True}
|
|
969
|
+
success = True
|
|
970
|
+
except:
|
|
971
|
+
traceback.print_exc()
|
|
972
|
+
success = False
|
|
973
|
+
out1 = {'success':False}
|
|
974
|
+
|
|
975
|
+
# Remove outliers and refit
|
|
976
|
+
if success and outlier:
|
|
977
|
+
diff = pspec['flux']-bestmodel
|
|
978
|
+
med = np.median(diff)
|
|
979
|
+
sig = dln.mad(diff)
|
|
980
|
+
bd, = np.where(np.abs(diff) > 3*sig)
|
|
981
|
+
nbd = len(bd)
|
|
982
|
+
if nbd>0:
|
|
983
|
+
if verbose:
|
|
984
|
+
print('Removing '+str(nbd)+' outliers and refitting')
|
|
985
|
+
err = pspec['err'].copy()
|
|
986
|
+
flux = pspec['flux'].copy()
|
|
987
|
+
err[bd] = 1e30
|
|
988
|
+
flux[bd] = bestmodel[bd]
|
|
989
|
+
# Save original values
|
|
990
|
+
pars0 = pars
|
|
991
|
+
estimates = pars0
|
|
992
|
+
# Run curve_fit
|
|
993
|
+
try:
|
|
994
|
+
pars,pcov = curve_fit(fr.model,pspec['wave'],flux,p0=estimates,
|
|
995
|
+
sigma=err,bounds=bounds,jac=fr.jac)
|
|
996
|
+
perror = np.sqrt(np.diag(pcov))
|
|
997
|
+
bestmodel = fr.model(pspec['wave'],*pars)
|
|
998
|
+
chisq = np.sum((flux-bestmodel)**2/err**2)/len(flux)
|
|
999
|
+
|
|
1000
|
+
# Get full parameters
|
|
1001
|
+
if loggrelation:
|
|
1002
|
+
fullpars = fr.getlogg(pars)
|
|
1003
|
+
fullperror = np.insert(perror,fr.loggind,0.0)
|
|
1004
|
+
else:
|
|
1005
|
+
fullpars = pars
|
|
1006
|
+
fullperror = perror
|
|
1007
|
+
|
|
1008
|
+
if verbose:
|
|
1009
|
+
printvals = [fullpars[0],fullperror[0],fullpars[1],fullperror[1],fullpars[2],fullperror[2],
|
|
1010
|
+
fullpars[3],fullperror[3]]
|
|
1011
|
+
print('Best parameters: {:f}+/-{:f}, {:.3f}+/-{:.3f}, {:.3f}+/-{:.3f}, {:.3f}+/-{:.3f}'.format(*printvals))
|
|
1012
|
+
print('Chisq: ',chisq)
|
|
1013
|
+
|
|
1014
|
+
# Construct the output dictionary
|
|
1015
|
+
out1 = {'index':i,'vrel':vrel1,'snr':spec.snr,'pars':fullpars,'perror':fullperror,'wave':pspec['wave'],
|
|
1016
|
+
'flux':pspec['flux'],'err':pspec['err'],'mflux':flux,'merr':err,'noutlier':nbd,'model':bestmodel,
|
|
1017
|
+
'chisq':chisq,'loggrelation':loggrelation,'success':True}
|
|
1018
|
+
success = True
|
|
1019
|
+
except:
|
|
1020
|
+
traceback.print_exc()
|
|
1021
|
+
success = False
|
|
1022
|
+
out1 = {'success':False}
|
|
1023
|
+
|
|
1024
|
+
#if verbose and success:
|
|
1025
|
+
# print('Best parameters: ',out1['pars'])
|
|
1026
|
+
# print('Chisq: ',out1['chisq'])
|
|
1027
|
+
|
|
1028
|
+
out.append(out1)
|
|
1029
|
+
|
|
1030
|
+
# Create output table
|
|
1031
|
+
dt = [('index',int),('vrel',float),('snr',float),('pars',float,4),('perror',float,4),('chisq',float),('success',bool)]
|
|
1032
|
+
tab = np.zeros(len(out),dtype=np.dtype(dt))
|
|
1033
|
+
for i,o in enumerate(out):
|
|
1034
|
+
if o['success']:
|
|
1035
|
+
tab['index'][i] = o['index']
|
|
1036
|
+
tab['vrel'][i] = o['vrel']
|
|
1037
|
+
tab['snr'][i] = o['snr']
|
|
1038
|
+
tab['pars'][i] = o['pars']
|
|
1039
|
+
tab['perror'][i] = o['perror']
|
|
1040
|
+
tab['chisq'][i] = o['chisq']
|
|
1041
|
+
tab['success'][i] = o['success']
|
|
1042
|
+
tab = Table(tab)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
return out,tab
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ferre
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Spectral analysis code with synthetic spectral grid
|
|
5
|
+
Home-page: https://github.com/dnidever/ferre
|
|
6
|
+
Author: Carlos Allende Prieto, David Nidever
|
|
7
|
+
Author-email: carlos.allende.prieto@iac.es,dnidever@montana.edu
|
|
8
|
+
Requires: numpy
|
|
9
|
+
Requires: astropy(>=4.0)
|
|
10
|
+
Requires: scipy
|
|
11
|
+
Requires: matplotlib
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ferre
|
ferre-1.0.0/setup.cfg
ADDED
ferre-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import fileinput
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
from setuptools.command.install import install
|
|
7
|
+
from setuptools import setup, find_packages
|
|
8
|
+
|
|
9
|
+
from platform import system as current_platform
|
|
10
|
+
import shutil
|
|
11
|
+
from glob import glob
|
|
12
|
+
|
|
13
|
+
def get_bin_path():
|
|
14
|
+
# Get environment bin/ directory
|
|
15
|
+
bindir = None
|
|
16
|
+
# check if the --user option was set
|
|
17
|
+
userdir = None
|
|
18
|
+
for u in install.user_options:
|
|
19
|
+
if u[0]=='user':
|
|
20
|
+
# "install in user site-package '/Users/nidever/.local/lib/python3.7/site-packages'"
|
|
21
|
+
uline = u[2]
|
|
22
|
+
userdir = uline[uline.find('site-package')+13:]
|
|
23
|
+
userdir = userdir.replace("'","")
|
|
24
|
+
if userdir is not None:
|
|
25
|
+
# /Users/nidever/.local/bin
|
|
26
|
+
bindir = os.path.dirname(os.path.dirname(os.path.dirname(userdir)))+'/bin'
|
|
27
|
+
if os.path.exists(bindir)==False:
|
|
28
|
+
bindir = False
|
|
29
|
+
# Try virtual environment using sys.prefix
|
|
30
|
+
if bindir is None:
|
|
31
|
+
venv = get_virtualenv_path()
|
|
32
|
+
if venv is not None:
|
|
33
|
+
bindir = venv+'/bin'
|
|
34
|
+
if os.path.exists(bindir)==False:
|
|
35
|
+
bindir = None
|
|
36
|
+
# Get bin/ directory from python executable
|
|
37
|
+
if bindir is None:
|
|
38
|
+
out = subprocess.run(['which','python'],shell=True)
|
|
39
|
+
if type(out) is bytes:
|
|
40
|
+
out = out.decode()
|
|
41
|
+
bindir = os.path.dirname(out)
|
|
42
|
+
if os.path.exists(bindir)==False:
|
|
43
|
+
bindir = None
|
|
44
|
+
|
|
45
|
+
if bindir is None:
|
|
46
|
+
raise Exception('No bin/ directory found')
|
|
47
|
+
|
|
48
|
+
return bindir
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def compileferre():
|
|
52
|
+
""" Compile the FERRE Fortran code."""
|
|
53
|
+
src_path = './src/'
|
|
54
|
+
|
|
55
|
+
# Identify the platform
|
|
56
|
+
platform = current_platform()
|
|
57
|
+
# Check for platform first
|
|
58
|
+
#if platform not in ('Darwin', 'Linux'):
|
|
59
|
+
# sys.stderr.write("Platform '%s' not recognised!\n" % platform)
|
|
60
|
+
# sys.exit()
|
|
61
|
+
|
|
62
|
+
# Install the software
|
|
63
|
+
print('Compiling the Fortran code')
|
|
64
|
+
ret = subprocess.run(['make'], cwd=src_path, shell=True)
|
|
65
|
+
|
|
66
|
+
# Get the path for the binaries
|
|
67
|
+
bindir = get_bin_path()
|
|
68
|
+
|
|
69
|
+
# Copy fortran binaries to bin/ directory
|
|
70
|
+
for f in ['ferre','ascii2bin']:
|
|
71
|
+
if os.path.exists('bin/'+f):
|
|
72
|
+
if os.path.exists(bindir+f): os.remove(bindir+f)
|
|
73
|
+
print('Copying bin/'+f+' -> '+bindir+'/'+f)
|
|
74
|
+
shutil.copyfile('bin/'+f,bindir+'/'+f)
|
|
75
|
+
# Make executable
|
|
76
|
+
os.chmod(bindir+'/'+f,0o755)
|
|
77
|
+
else:
|
|
78
|
+
print('bin/'+f+' NOT FOUND')
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# We need to build FERRE
|
|
82
|
+
if 'install' in sys.argv or 'develop' in sys.argv or 'bdist_wheel' in sys.argv:
|
|
83
|
+
compileferre()
|
|
84
|
+
|
|
85
|
+
setup(name='ferre',
|
|
86
|
+
version='1.0.0',
|
|
87
|
+
description='Spectral analysis code with synthetic spectral grid',
|
|
88
|
+
author='Carlos Allende Prieto, David Nidever',
|
|
89
|
+
author_email='carlos.allende.prieto@iac.es,dnidever@montana.edu',
|
|
90
|
+
url='https://github.com/dnidever/ferre',
|
|
91
|
+
requires=['numpy','astropy(>=4.0)','scipy','matplotlib'],
|
|
92
|
+
packages = ['ferre'],
|
|
93
|
+
package_dir={"": "python"}
|
|
94
|
+
)
|