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 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,8 @@
1
+ README.rst
2
+ setup.py
3
+ python/ferre/__init__.py
4
+ python/ferre/ferre.py
5
+ python/ferre.egg-info/PKG-INFO
6
+ python/ferre.egg-info/SOURCES.txt
7
+ python/ferre.egg-info/dependency_links.txt
8
+ python/ferre.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ ferre
ferre-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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
+ )