diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f71a56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +*~ +*.pyc +.DS_Store +00ToDo \ No newline at end of file diff --git a/.gitignore~ b/.gitignore~ new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/.gitignore~ @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 6f6c895..8ff3c3d 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ additional command line arguments --md MD metallicity scales to calculate. default is 'all', options are: D02, Z94, M91, M08, P05,P10, PP04, M13, D13, KD02, KD02comb, DP00 (deprecated), P01 (deprecated), C01 (deprecated) + --unpickle if it exists, a pickle file generated in a previous run for this \ and this number of samples is read in instead of recalculating the metalicity @@ -72,6 +73,8 @@ additional command line arguments --nodust don't do dust corrections (default is to do it) + --bluedust Calculate extinction based on Hg/Hb (default is to do Hb/Ha) + --noplot don't plot individual distributions (default is to plot all distributions) diff --git a/build/lib/pyMCZ/__init__.py b/build/lib/pyMCZ/__init__.py new file mode 100644 index 0000000..e17c7f3 --- /dev/null +++ b/build/lib/pyMCZ/__init__.py @@ -0,0 +1,7 @@ +import os + +p = os.path.abspath('./') +if not os.path.exists(p + '/output'): + os.makedirs(p + '/output') + +__all__ = ["mcz", "metallicity", "metscales", "testcompleteness"] diff --git a/build/lib/pyMCZ/mcz.py b/build/lib/pyMCZ/mcz.py new file mode 100644 index 0000000..8fa30cd --- /dev/null +++ b/build/lib/pyMCZ/mcz.py @@ -0,0 +1,791 @@ +#!/usr/bin/env python +from __future__ import print_function +import os +import sys +import argparse +import warnings + +import numpy as np +import scipy.stats as stats +from scipy.special import gammaln +from scipy import optimize +import matplotlib.pyplot as plt +from matplotlib.ticker import FormatStrFormatter +import csv as csv + +#modules of this package +import pylabsetup + +#import metallicity_save2 as metallicity +import metallicity as metallicity + +import itertools +import multiprocessing as mpc + +###versin 1.3 February 2016 +#fixed compatyibiity issue with numphy >1.9 in the val to np.histogram https://github.com/numpy/numpy/issues/6469 + +###version 1.3.1 April 2016 +#works with version 0.7 of pyqz (current version) +#previous versions only worked with version 0.5. +#a check for the version of pyqz is implememted and a different call is issued for version >0.5 and <=0.5 + + + +# Define the version of +__version__ = '1.3.1' + +NM0 = 0 # setting this to say N>0 starts the calculation at measurement N. +#this is only for exploratory purposes as the code bugs out before +#plotting and printing the results + +PROFILING = True +PROFILING = False + +alllines = ['[OII]3727', 'Hg','Hb', '[OIII]4959', '[OIII]5007', '[OI]6300', 'Ha', '[NII]6584', '[SII]6717', '[SII]6731', '[SIII]9069', '[SIII]9532'] +morelines = ['E(B-V)', 'dE(B-V)', 'scale_blue', 'd scale_blue'] + +MAXPROCESSES = 10 + +#pickle may not be installed +NOPICKLE = False +try: + import pickle +except ImportError: + NOPICKLE = True + +CLOBBER = False +VERBOSE = False +UNPICKLE = False +ASCIIOUTPUT = False +ASCIIDISTRIB = False +RUNSIM = True +NOPLOT = False +BINMODE = 'k' +binning = {'bb': 'Bayesian blocks', 'k': "Knuth's rule", + 'd': "Doane's formula", 's': r'$\sqrt{N}$', + 't': r'$2 N^{1/3}$', 'kd': 'Kernel Density'} + +MP = False + + +def is_number(s): + if not type(s) is np.string_: + try: + float(s) + return True + except ValueError: + return False + return False + + +def smart_open(logfile=None): + if logfile and logfile != '-': + fh = open(logfile, 'w') + else: + fh = sys.stdout + return fh + + +def getknuth(m, data, N): + m = int(m) + if m > N: + return [-1] + bins = np.linspace(min(data), max(data), int(m) + 1) + try: + nk, bins = np.histogram(data, bins) + return -(N * np.log(m) + gammaln(0.5 * m) - m * gammaln(0.5) - gammaln(N + 0.5 * m) + np.sum(gammaln(nk + 0.5))) + except: + return [-1] + + +def knuthn(data, maxM=None): + assert data.ndim == 1, "data must be 1D array to calculate Knuth's number of bins" + N = data.size + if not maxM: + maxM = 5 * np.sqrt(N) + m0 = 2.0 * N ** (1. / 3.) + gk = getknuth + if gk == [-1]: + return m0, 't' + mkall = optimize.fmin(gk, m0, args=(data, N), disp=VERBOSE, maxiter=30) # , maxfun=1000)#[0] + mk = mkall[0] + if mk > maxM or mk < 0.3 * np.sqrt(N): + mk = m0 + return mk, 't' + return mk, 0 + + +############################################################################## +##The input data +############################################################################## +##Reads the flux file and returns it as an array. +##Ignores non-numeric lines +##Returns (flux array,num) +############################################################################## +def readfile(filename): + noheader = 1 + findex = -1 + f = open(filename, 'r') + l0 = f.readline().replace(' ', '') + l1 = f.readline().split() + if l0.startswith('#') or l0.startswith(';'): + # Modified to account for non-standard header (\t instead of , separated) jch + temp_header = l0.strip().replace(";", '').replace("#", '') + if temp_header.find(',') == -1: + header = temp_header.split('\t') + else: + header = temp_header.split(',') + header[0] = header[0].replace(' ', '') + header = header[:len(l1)] + else: + noheader = 0 + header = ['galnum'] + alllines + ['flag'] + morelines + header = header[:len(l1)] + + usecols = [] + newheader = [] + for i,h in enumerate(header): + if h in alllines + ['galnum']: + usecols.append(i) + newheader.append(h) + + header = newheader + + formats = ['S10'] + ['f'] * (len(header) - 1) + + if 'flag' in header: + findex = header.index('flag') + formats[findex] = 'S10' + + bstruct = {} + for i, k in enumerate(header): + bstruct[k] = [i, 0] + b = np.loadtxt(filename, skiprows=noheader, dtype={'names': header, 'formats': formats}, comments=';',usecols=usecols) + if b.size == 1: + b = np.atleast_1d(b) + for i, k in enumerate(header): + if not k == 'flag' and is_number(b[k][0]): + bstruct[k][1] = np.count_nonzero(b[k]) + sum(np.isnan(b[k])) + j = len(b['galnum']) + return b, j, bstruct + + +def ingest_data(filename, path): + ###Initialize### + measfile = os.path.join(path, filename + "_meas.txt") + errfile = os.path.join(path, filename + "_err.txt") + + ###read the max, meas, min flux files### + meas, nm, bsmeas = readfile(measfile) + err, nm, bserr = readfile(errfile) + try: + snr = (meas[: + ,1:].view(np.float32).reshape(meas[: + ,1:].shape + (-1, ))[: + ,1:]) / (err[: + ,1:].view(np.float32).reshape(err[: + ,1:].shape + (-1, ))[: + ,1:]) + if snr[~np.isnan(snr)].any() < 3: + raw_input('''WARNING: signal to noise ratio smaller than 3 + for at least some lines! You should only use SNR>3 + measurements (return to proceed)''') + except (IndexError, TypeError): + pass + return (filename, meas, err, nm, path, (bsmeas, bserr)) + +def input_data(filename, path): + p = os.path.join(path, "input") + assert os.path.isdir(p), "bad data directory %s" % p + if os.path.isfile(os.path.join(p, filename + '_err.txt')): + if os.path.isfile(os.path.join(p, filename + '_meas.txt')): + return ingest_data(filename, path=p) + print ("Unable to find _meas and _err files ", filename + + '_meas.txt', filename + '_err.txt', "in directory ", p) + return -1 + +############################################################################## +##returns a random distribution. In the deployed version of the code this is a gaussian distribution, but the user can include her or his distribution. +##return a random sample of size n +############################################################################## +def errordistrib(distrargs, n, distype='normal'): + if distype == 'normal': + try: + mu, sigma = distrargs + except: + print ("for normal distribution distargs must be a 2 element tuple") + return -1 + return np.random.normal(mu, sigma, n) + else: + print ("distribution not supported") + return -1 + + +############################################################################## +##returns appropriate bin size for the number of data +##mode 'k' calculates this based on Knuth's rule +##mode 'd' calculates this based on Doane's formula +##mode 's' calculates this based on sqrt of number of data +##mode 't' calculates this based on 2*n**1/3 (default) +############################################################################## +def getbinsize(n, data, ): + if BINMODE == 'd': + g1 = np.abs(stats.mstats.moment(data, moment=3)) # /data.std()) + s1 = np.sqrt(float(n) / 6.0) + #s1=1.0/np.sqrt(6.*(n-2.)/((n+1.)*(n+3.))) + k = 1 + np.log2(n) + np.log2(1 + (g1 * s1)), 0 + elif BINMODE == 's': + k = np.sqrt(n), 0 + elif BINMODE == 't': + k = 2. * n ** (1. / 3.), 0 + else: + k = knuthn(data) + return k + + +############################################################################## +##Check if hist files already exist and need to be replaced +############################################################################## +def checkhist(snname, Zs, nsample, i, path): + global CLOBBER + + name = '%s_n%d_%s_%d' % ((snname, nsample, Zs, i + 1)) + outdir = os.path.join(path, 'hist') + outfile = os.path.join(outdir, name + ".pdf") + if os.path.isfile(outfile) and not CLOBBER: + replace = raw_input("replacing existing image files, starting with: %s ? [Y/n]\n" % outfile).lower() + assert(not (replace.startswith('n'))), "save your existing output directory under another name first" + CLOBBER = True + + +############################################################################## +##Save the result as histogram as name +############################################################################## +#@profile +def savehist(data, snname, Zs, nsample, i, path, nmeas, measnames, verbose=False, fs=24, reserr=None): + global BINMODE + #global NOPLOT + + name = '%s_n%d_%s_%d' % ((snname, nsample, Zs, i + 1)) + outdir = os.path.join(path, 'hist') + outfile = os.path.join(outdir, name + ".pdf") + if not NOPLOT: + fig = plt.figure(figsize=(11, 8)) + plt.clf() + + ####kill outliers, infinities, and bad distributions### + data = data[np.isfinite(data)] + + n = data.shape[0] + kde = None + if not n > 0: + if verbose: + print ("data must be an actual distribution (n>0 elements!, %s)" % Zs) + return "-1,-1,_1", [], kde + + if data.shape[0] <= 0 or np.sum(data) <= 0: + print ('{0:15} {1:20} {2:>13d} {3:>7d} {4:>7d} '\ + .format(snname, Zs, -1, -1, -1)) + return "-1, -1, -1", [], kde + try: + ###find C.I.### + median, pc16, pc84 = np.percentile(data, [50, 16, 84]) + std = np.std(data) + left = pc16 + right = pc84 + maxleft = median - std * 5 + maxright = median + std * 5 + if "%2f" % maxright == "%2f" % maxleft: + maxleft = median - 1 + maxright = median + 1 + if round(right, 6) == round(left, 6) and round(left, 6) == round(median, 6): + print ('{0:15} {1:20} {2:>13.3f} -{3:>7.3f} +{4:>7.3f} (no distribution)'.format(snname, Zs, median, 0, 0)) + if reserr: + print ('+/- {0:.3f}'.format(reserr)) + return "%f\t %f\t %f" % (round(median, 3), round(median - left, 3), round(right - median, 3)), data, kde # "-1,-1,-1",[] + ###print out the confidence interval### + print ('{0:15} {1:20} {2:>13.3f} -{3:>7.3f} +{4:>7.3f}'.format(snname, + Zs, round(median, 3), + round(median - left, 3), + round(right - median, 3))) + if reserr: + print ('+/- {0:.3f}'.format(reserr)) + alpha = 1.0 + + ######histogram###### + if BINMODE == 'kd': + ##if sklearn is available use it to get Kernel Density + try: + from sklearn.neighbors import KernelDensity + except ImportError: + print ('''sklearn is not available, + thus we cannot compute kernel density. + switching to bayesian blocks''') + BINMODE = 'bb' + if BINMODE == 'kd': + ##bw is chosen according to Silverman 1986 + bw = 1.06 * std * n ** (-0.2) + numbin, bm = getbinsize(data.shape[0], data) + distrib = np.histogram(data, bins=int(numbin), density=True) + ###make hist### + counts, bins = distrib[0], distrib[1] + widths = np.diff(bins) + countsnorm = counts / np.max(counts) + + if bw > 0: + kde = KernelDensity(kernel='gaussian', bandwidth=bw).fit(data[: + , np.newaxis]) + kdebins = np.linspace(maxleft, maxright, 1000)[: + , np.newaxis] + log_dens = kde.score_samples(kdebins) + dens = np.exp(log_dens) + norm = countsnorm.sum() * (bins[1] - bins[0]) / dens.sum() / (kdebins[1] - kdebins[0]) + if not NOPLOT: + plt.fill(kdebins[: + ,0], dens * norm, fc='#7570b3', alpha=0.8) + alpha = 0.5 + + ###find appropriate bin size### + else: + if BINMODE == 'bb': + ##if astroML is available use it to get Bayesian blocks + bm = 0 + try: + from astroML.plotting import hist as amlhist + if BINMODE == 'bb': + distrib = amlhist(data, bins='blocks', normed=True) + if not NOPLOT: + plt.clf() + except ImportError: + print ("bayesian blocks for histogram requires astroML to be installed") + print ("defaulting to Knuth's rule ") + ##otherwise + numbin, bm = getbinsize(data.shape[0], data) + distrib = np.histogram(data, bins=int(numbin), density=True) + else: + numbin, bm = getbinsize(data.shape[0], data) + distrib = np.histogram(data, bins=int(numbin), density=True) + + ###make hist### + counts, bins = distrib[0], distrib[1] + widths = np.diff(bins) + countsnorm = counts / np.max(counts) + + ###plot hist### + if NOPLOT: + return "%f\t %f\t %f" % (round(median, 3), round(median - left, 3), round(right - median, 3)), data, kde + + plt.bar(bins[:-1], countsnorm, widths, color=['gray'], alpha=alpha) + plt.minorticks_on() + plt.gca().xaxis.set_major_formatter(FormatStrFormatter('%.2f')) + plt.xlim(maxleft, maxright) + + #the following lines assure the x tick label is + #within the length of the x axis + xticks = plt.xticks()[0] + dx = xticks[-1] - xticks[-2] + xticks = xticks[(xticks < maxright) * (xticks > maxleft)] + if (maxright - xticks[-1]) < 0.25 * dx: + maxright = maxright + 0.25 * dx + maxleft = maxleft - 0.25 * dx + plt.xlim(maxleft, maxright) + plt.xticks(xticks, ['%.2f' % s for s in xticks]) + + plt.ylim(0, 1.15) + plt.yticks(np.arange(0.2, 1.3, 0.2), ["%.1f" % x for x in np.arange(0.2, 1.1, 0.2)]) + plt.axvspan(left, right, color='DarkOrange', alpha=0.4) + plt.axvline(x=median, linewidth=2, color='white', ls='--') + + #labels and legends + st = '%s ' % (snname) + plt.annotate(st, xy=(0.13, 0.6), xycoords='axes fraction', size=fs, fontweight='bold') + st = '%s ' % (Zs.replace('_', ' ')) + plt.annotate(st, xy=(0.61, 0.93), xycoords='axes fraction', fontsize=fs, fontweight='bold') + st = 'measurement %d of %d\n %s\nmedian: %.3f\n16th Percentile: %.3f\n84th Percentile: %.3f' \ + % (i + 1, nmeas, measnames[i], round(median, 3), round(left, 3), round(right, 3)) + plt.annotate(st, xy=(0.61, 0.65), xycoords='axes fraction', fontsize=fs) + effectiven = len(data[~np.isnan(data)]) + if effectiven: + st = 'MC sample size %d (%d)\nhistogram rule: %s' % (effectiven, nsample, binning[BINMODE]) + if bm: + if effectiven < nsample: + st = 'MC sample size %d (%d)\nhistogram rule: %s' % (effectiven, nsample, binning[bm]) + else: + st = 'MC sample size %d\nhistogram rule: %s' % (nsample, binning[bm]) + plt.annotate(st, xy=(0.61, 0.55), xycoords='axes fraction', fontsize=fs - 5) + if "E(B-V)" in Zs: + plt.xlabel('E(B-V) [mag]') + outfile = outfile.replace('(', '').replace(')', '') + elif "logR23" in Zs: + plt.xlabel('logR23') + else: + plt.xlabel('12+log(O/H)') + plt.ylabel('relative counts') + plt.savefig(outfile, format='pdf') + plt.close(fig) + + return "%f\t %f\t %f" % (round(median, 3), round(median - left, 3), round(right - median, 3)), data, kde + + except (OverflowError, AttributeError, ValueError): + if VERBOSE: + print (data) + print (name, 'had infinities (or something in plotting went wrong)') + return "-1, -1,-1", [], None + + +def calc((i, (sample, flux, err, nm, bss, mds, disp, dust_corr,dust_blue, + verbose, res, scales, nps, logf))): + logf = sys.stdout + logf.write("\n\nreading in measurements %d\n"%(i + 1)) + fluxi = {} # np.zeros((len(bss[0]),nm),float) + for k in bss[0].iterkeys(): + logf.write('{0:15} '.format(k)) + logf.write('{0:0.2} +/- {1:0.2}\n'.format(flux[k][i], err[k][i])) + #print >> logf, '{0:15} '.format(k), + #print >> logf, '{0:0.2} +/- {1:0.2}'.format(flux[k][i], err[k][i]) + fluxi[k] = flux[k][i] * np.ones(len(sample[i])) + err[k][i] * sample[i] + warnings.filterwarnings("ignore") + success = metallicity.calculation(scales[i], fluxi, nm, mds, nps, logf, + disp=disp, verbose=verbose, + dust_corr=dust_corr,dust_blue=dust_blue) + if success == -1: + logf.write("MINIMUM REQUIRED LINES: '[OII]3727','[OIII]5007','[NII]6584','[SII]6717'\n") + #print >> logf, "MINIMUM REQUIRED LINES: '[OII]3727','[OIII]5007','[NII]6584','[SII]6717'" + + for key in scales[i].mds.iterkeys(): + if key in res.keys(): + res[key][i] = scales[i].mds[key] + if res[key][i] is None: + res[key][i] = [float('NaN')] * len(sample) + return res + + +############################################################################## +## The main function. takes the flux and its error as input. +## filename - a string 'filename' common to the three flux files +## flux - np array of the fluxes +## err - the flux errors, must be the same dimension as flux +## nsample - the number of samples the code will generate. Default is 100 +## errmode - determines which method to choose the bin size. +## mode 'k' calculates this based on Knuth's rule (default) +## mode 'd' calculates this based on Doane's formula +## mode 's' calculates this based on sqrt of number of data +## mode 't' calculates this based on 2*n**1/3 +############################################################################## +#@profile +def run((name, flux, err, nm, path, bss), nsample, mds, multiproc, logf, + unpickle=False, dust_corr=True, dust_blue=False, verbose=False, fs=24): + global RUNSIM # ,BINMODE#,NOPLOT + assert(len(flux[0]) == len(err[0])), "flux and err must be same dimensions" + assert(len(flux['galnum']) == nm), "flux and err must be of declaired size" + assert(len(err['galnum']) == nm), "flux and err must be same dimensions" + + #increasing sample by 10% to assure robustness against rejected samples + newnsample = nsample + if nsample > 1: + newnsample = int(nsample + 0.1 * nsample) + + p = os.path.join(path, '..') + + ###retrieve the metallicity keys + Zs = metallicity.get_keys() + Zserr = metallicity.get_errkeys() + + ###make necessary paths for output files + if not os.path.exists(os.path.join(p, 'output', '%s' % name)): + os.makedirs(os.path.join(p, 'output', '%s' % name)) + if not os.path.exists(os.path.join(p, 'output', '%s' % name, 'hist')): + os.makedirs(os.path.join(p, 'output', '%s' % name, 'hist')) + binp = os.path.join(p, 'output', '%s' % name) + picklefile = os.path.join(binp, '%s_n%d.pkl' % (name, nsample)) + if VERBOSE: + print ("output files will be stored in ", binp) + if not CLOBBER and not NOPLOT: + for key in Zs: + for i in range(NM0, nm): + checkhist(name, key, nsample, i, binp) + if unpickle: + RUNSIM = False + if not os.path.isfile(picklefile): + raw_input("missing pickled file for this simulation: name, nsample.\nrun the MonteCarlo? Ctr-C to exit, Return to continue?\n") + RUNSIM = True + else: + pklfile = open(picklefile, 'rb') + res = pickle.load(pklfile) + + if RUNSIM: + ###Sample 'nsample' points from a gaussian centered on 0 with std 1 + mu = 0 + sigma = 1 + dargs = (mu, sigma) + if nsample == 1: + sample = [np.array([mu]) for i in range(NM0, nm)] + else: + sample = [errordistrib(dargs, newnsample) for i in range(NM0, nm)] + if sample == -1: + return -1 + + ###Start calculation### + ## the flux to be feed to the calculation will be + ## flux + error*i + ## where i is the sampled gaussian + if VERBOSE: + print ("Starting iteration") + + #initialize the dictionary + res = {} + reserr = {} + for key in Zs: + res[key] = [[] for i in range(NM0, nm)] + for key in Zserr: + reserr[key] = [[] for i in range(NM0, nm)] + #use only valid inputs + delkeys = [] + for k in bss[0].iterkeys(): + if k == 'flag' or k == 'galnum' or bss[0][k][1] == float('nan'): # or bss[1][k][1]==bss[0][k][1]: + delkeys.append(k) + for k in delkeys: + del bss[0][k] + del bss[1][k] + + import metscales as ms + nps = min(mpc.cpu_count() - 1 or 1, MAXPROCESSES) + + if multiproc and nps > 1: + scales = [ms.diagnostics(newnsample, logf, nps) for i in range(nm)] + + logf.write( "\n\n\nrunning on %d threads\n\n\n" % nps) + #print >> logf, "\n\n\nrunning on %d threads\n\n\n" % nps + second_args = [sample, flux, err, nm, bss, mds, VERBOSE, dust_corr, dust_blue, VERBOSE, res, scales, nps, logf] + pool = mpc.Pool(processes=nps) # depends on available cores + rr = pool.map(calc, itertools.izip(range(NM0, nm), itertools.repeat(second_args))) # for i in range(nm): result[i] = f(i, second_args) + + + for ri, r in enumerate(rr): + for kk in r.iterkeys(): + res[kk][ri] = r[kk][ri] + + pool.close() # not optimal! but easy + pool.join() + for key in scales[i].mds.iterkeys(): + if key in Zs: + res[key] = np.array(res[key]).T + elif key in reserr.keys(): + reserr[key] = np.array(reserr[key]).T + if res[key][i] is None: + res[key][i] = [float('NaN')] * len(sample) + + if VERBOSE: + print ("Iteration Complete") + else: + #looping over nm spectra + for i in range(NM0, nm): + scales = ms.diagnostics(newnsample, logf, nps) + logf.write("\n\n measurements %d\n"%(i + 1)) + #print >> logf, "\n\n measurements ", i + 1 + fluxi = {} + for k in bss[0].iterkeys(): + #shuffling the distribution each time + np.random.shuffle(sample[i]) + + logf.write('{0:15} '.format(k)) + logf.write('{0:0.2} +/- {1:0.2}\n'.format(flux[k][i], + err[k][i])) + #print >> logf, '{0:15} '.format(k), + #print >> logf, '{0:0.2} +/- {1:0.2}'.format(flux[k][i], err[k][i]) + fluxi[k] = flux[k][i] * np.ones(len(sample[i])) + err[k][i] * sample[i] + warnings.filterwarnings("ignore") + logf.write("\n") + #print >> logf, "" + + success = metallicity.calculation(scales, fluxi, nm, mds, 1, logf, + disp=VERBOSE, verbose=VERBOSE, + dust_corr=dust_corr, dust_blue=dust_blue) + if success == -1: + print ("MINIMUM REQUIRED LINES: [OII]3727 & [OIII]5007, or [NII]6584, and Ha & Hb if you want dereddening") + #continue + + + for key in scales.mds.iterkeys(): + if key not in Zs: + continue + res[key][i] = scales.mds[key] + if res[key][i] is None: + res[key][i] = [float('NaN')] * newnsample + elif len(res[key][i]) < newnsample: + res[key][i] = res[key][i] + [float('NaN')] * (newnsample - len(res[key][i])) + + for key in scales.mds.iterkeys(): + if key in Zs: + res[key] = np.array(res[key]).T + if VERBOSE: + print ("Iteration Complete") + + #"WE CAN PICKLE THIS!" + #pickle this realization + if not NOPICKLE: + pickle.dump(res, open(picklefile, 'wb')) + + from matplotlib.font_manager import findfont, FontProperties + + if 'Time' not in findfont(FontProperties()): + fs = 20 + if VERBOSE: + print ("FONT: %s, %d" % (findfont(FontProperties()), fs)) + + ###Bin the results and save### + print ("\n\n") + print ('{0:15} {1:20} {2:>13} -{3:>5} +{4:>5} {5:11} {6:>7}'\ + .format("SN", "diagnostic", "metallicity", "34%", "34%", + "(sample size:", '%d)' % nsample)) + for i in range(NM0, nm): + if ASCIIOUTPUT: + fi = open(os.path.join(binp, '%s_n%d_%d.txt' % (name, nsample, i + 1)), 'w') + fi.write("%s\t Median Oxygen abundance (12+log(O/H))\t 16th percentile\t 84th percentile\n" % name) + + boxlabels = [] + datas = [] + print ("\n\nmeasurement ", + "%d/%d : %s-------------------------------------------------------------" % (i + 1, nm, flux[i]['galnum'])) + for key in Zs: + if nsample == -1: + try: + if ~np.isnan(res[key][i][0]): + print ('{0:15} {1:20} {2:>13.3f} -{3:>7.3f} +{4:>7.3f} (no distribution)'.format(name + ' %d' % (i + 1), key, res[key][i][0], 0, 0)) + except IndexError: + pass + else: + reserr = None + try: + if sum(~np.isnan(res[key][: + ,i])) > 0: + if ASCIIDISTRIB: + with open(os.path.join(binp, '%s_n%d_%s_%d.csv' % (name, nsample, key, i + 1)), "wb") as fidist: + writer = csv.writer(fidist) + writer.writerow(res[key][: + ,i]) + if 'PM14' in key: + print (reserr['PM14err']) + reserr = np.sqrt(~np.nansum(reserr['PM14err'][: + ,i] ** 2)) + sh, data, kde = savehist(res[key][: + ,i], name, key, nsample, i, binp, nm, flux[:]['galnum'], verbose=verbose, fs=fs, reserr=reserr) + s = key + "\t " + sh + '\n' + if ASCIIOUTPUT: + fi.write(s) + if key not in ["E(B-V)", "logR23"]: + boxlabels.append(key.replace('_', ' ')) + datas.append(data) + if BINMODE == 'kd' and not NOPICKLE: + pickleKDEfile = os.path.join(binp + '/%s_n%d_%s_%d_KDE.pkl' % (name, nsample, key, i + 1)) + if VERBOSE: + print ("KDE files will be stored in ", pickleKDEfile) + pickle.dump(kde, open(pickleKDEfile, 'wb')) + except (IndexError, TypeError): + pass + #make box_and_whiskers plot + fig = plt.figure(figsize=(8, 15)) + fig.subplots_adjust(bottom=0.18, left=0.18) + ax = fig.add_subplot(111) + plt.grid() + if len(datas) == 0: + continue + + bp = ax.boxplot(datas, patch_artist=True) + for box in bp['boxes']: + box.set(color='#7570b3', linewidth=2) + box.set(facecolor='DarkOrange', alpha=0.4) + for whisker in bp['whiskers']: + whisker.set(color='#7570b3', linewidth=2) + for cap in bp['caps']: + cap.set(color='#7570b3', linewidth=2) + for median in bp['medians']: + median.set(color='k', linewidth=2) + for flier in bp['fliers']: + flier.set(marker='o', color='#7570b3', alpha=0.4, mfc='#7570b3') + plt.title("measurement %d: %s" % (i + 1, flux[i]['galnum'])) + plt.xticks(range(1, len(boxlabels) + 1), boxlabels, rotation=90, fontsize=fs - 5) + plt.fill_between(range(1, len(boxlabels) + 1), [8.76] * len(boxlabels), [8.69] * len(boxlabels), facecolor='black', alpha=0.3) + plt.text(1.2, 8.705, "Solar Oxygen Abundance", alpha=0.7) + plt.gca().yaxis.set_major_formatter(FormatStrFormatter('%.2f')) + plt.ylabel('12+log(O/H)', fontsize=fs) + plt.savefig(binp + "/" + name + "_boxplot_n%d_%d.pdf" % (nsample, i + 1), format='pdf') + if ASCIIOUTPUT: + fi.close() + if VERBOSE: + print ("uncertainty calculation complete") + + #del datas + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('name', metavar='', type=str, help="the SN file name (root of the _min,_max file names") + parser.add_argument('nsample', metavar='N', type=int, help="number of iterations, minimum 100 (or 1 for no MC sampling)") + parser.add_argument('--clobber', default=False, action='store_true', help="replace existing output") + parser.add_argument('--binmode', default='k', type=str, choices=['d', 's', 'k', 't', 'bb', 'kd'], help='''method to determine bin size + {d: Duanes formula, s: n^1/2, t: 2*n**1/3(default), k: Knuth's rule, + bb: Bayesian blocks, kd: Kernel Density}''') + parser.add_argument('--path', default=None, type=str, help='''input/output path (must contain the input _max.txt and + _min.txt files in a subdirectory sn_data)''') + parser.add_argument('--unpickle', default=False, action='store_true', help="read the pickled realization instead of making a new one") + + parser.add_argument('--verbose', default=False, action='store_true', help="verbose mode") + parser.add_argument('--log', default=None, type=str, help="log file, if not passed defaults to standard output") + parser.add_argument('--nodust', default=False, action='store_true', help=" don't do dust corrections (default is to do it)") + parser.add_argument('--bluedust', default=False, action='store_true', help=" use Hg/Hb for dust correction (default is Hb/Ha)") + parser.add_argument('--noplot', default=False, action='store_true', help=" don't plot individual distributions (default is to plot all distributions)") + parser.add_argument('--asciiout', default=False, action='store_true', help=" write distribution in an ascii output (default is not to)") + parser.add_argument('--asciidistrib', default=False, action='store_true', help=" write distribution in an ascii output (default is not to)") + parser.add_argument('--md', default='all', type=str, help='''metallicity diagnostic to calculate. + default is 'all', options are: + D02, Z94, M91, C01, P05, M08, M08all, M13, PP04, D13, D13all, KD02, DP00 (deprecated), P01, D16, PG16, C17''') + parser.add_argument('--multiproc', default=False, action='store_true', help=" multiprocess, with number of threads max(available cores-1, MAXPROCESSES)") + args = parser.parse_args() + + global CLOBBER + global VERBOSE + global BINMODE + global ASCIIOUTPUT + global ASCIIDISTRIB + global NOPLOT + CLOBBER = args.clobber + VERBOSE = args.verbose + BINMODE = args.binmode + NOPLOT = args.noplot + ASCIIOUTPUT = args.asciiout + ASCIIDISTRIB = args.asciidistrib + + if args.unpickle and NOPICKLE: + args.unpickle = False + raw_input("cannot use pickle on this machine, we won't save and won't read saved pickled realizations. Ctr-C to exit, Return to continue?\n") + + if args.path: + path = args.path + else: + assert (os.getenv("MCMetdata")), ''' the _max, _min (and _med) data must live in a folder named sn_data. + pass a path to the sn_data folder, or set up the environmental variable + MCMetdata pointing to the path where sn_data lives ''' + path = os.getenv("MCMetdata") + assert(os.path.isdir(path)), "pass a path or set up the environmental variable MCMetdata pointing to the path where the _min _max _med files live" + if args.nsample == 1: + print ("CALCULATING METALLICITY WITHOUT GENERATING MC DISTRIBUTIONS") + if args.nsample == 1 or args.nsample >= 10: + fi = input_data(args.name, path=path) + if fi != -1: + logf = smart_open(args.log) + run(fi, args.nsample, args.md, args.multiproc, logf, + unpickle=args.unpickle, + dust_corr=(not args.nodust), dust_blue=(args.bluedust), + verbose=VERBOSE) + if args.log: + logf.close() + else: + print ("nsample must be at least 100") + +if __name__ == "__main__": + if PROFILING: + import cProfile + cProfile.run("main()") + else: + main() diff --git a/build/lib/pyMCZ/metallicity.py b/build/lib/pyMCZ/metallicity.py new file mode 100644 index 0000000..d5d2e79 --- /dev/null +++ b/build/lib/pyMCZ/metallicity.py @@ -0,0 +1,297 @@ +############################################################################## +## Calculates oxygen abundance (here called metalicity) based on strong emission lines, +## based on code originally written in IDL by Lisa Kewley (Kewley & Ellison 2008). Outputs +## oxygen abundance in many different diagnostics (see Bianco et al. 2016). +## +##new calculation based on the most recent version of the .pro file. +## +##inputs: +## measured - flux data, must be the format returned by readfile() +## num - number of spectra for which to calculate metallicity, also returned by readfile() +## outfilename - the name of the file the results will be appended to +## red_corr - reddening correction flag - True by default +## disp - if True prints the results, default False +############################################################################## + +from __future__ import print_function +import sys +import os +import numpy as np + +IGNOREDUST = False +MP = True +FIXNEGATIVES = True # set to true if no negative flux measurements should be allowed. all negative flux measurements are set to 0 +DROPNEGATIVES = False # set to true if no negative flux measurements should be allowed. all negative flux measurements are dropped +if DROPNEGATIVES: + FIXNEGATIVES = False + +##list of metallicity methods, in order calculated +Zs = ["E(B-V)", # based on Halpha, Hbeta + "E(B-V)blue", # based on Hbeta, Hgamma + "C(Hbeta)", + "logR23", # Hbeta, [OII]3727, [OIII]5007, [OIII]4959 + + "D02", # Halpha, [NII]6584 + "Z94", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "M91", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "C01_N2S2", # [OII]3727, [OIII]5007, [NII]6584, [SII]6717 + "C01_R23", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + + "P05", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "P01", # available but deprecated + "PP04_N2Ha", # Halpha, [NII]6584 + "PP04_O3N2", # Halpha, Hbeta,[OIII]5007, [NII]6584 + "DP00", # S23 available but deprecated + "P10_ONS", "P10_ON", + "M08_R23", "M08_N2Ha", "M08_O3Hb", "M08_O2Hb", "M08_O3O2", "M08_O3N2", + "M13_O3N2", "M13_N2Ha", + "D13_N2S2_O3S2", "D13_N2S2_O3Hb", + "D13_N2S2_O3O2", "D13_N2O2_O3S2", + "D13_N2O2_O3Hb", "D13_N2O2_O3O2", + "D13_N2Ha_O3Hb", "D13_N2Ha_O3O2", + "KD02_N2O2", # Halpha, Hbeta, [OII]3727, [NII]6584 + "KD02_N2S2", + "KK04_N2Ha", # Halpha, Hbeta, [OII]3727, [NII]6584 + "KK04_R23", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "KD02comb", + "PM14", + "PMC09_N2Ha", + "B07_N2O2", # Bresolin (2007) N2O2 + "D16", # Dopita (2016) ONS + "PG16_R","PG16_S", # Pilyugin & Grebel (2016) + "C17_O3O2", "C17_O3N2","C17_N2Ha","C17_O2Hb","C17_O3Hb","C17_R23"] + + +Zserr = ['PM14err'] # ,"KK04comb"] +#'KD02_N2O2', 'KD03new_R23', 'M91', 'KD03_N2Ha' + + +def get_keys(): + return Zs + + +def get_errkeys(): + return Zserr + + +def printsafemulti(string, logf, nps): + #this is needed because dealing with a log output with multiprocessing + #is painful. but it introduces a bunch of if checks. + if nps == 1: + logf.write(string + "\n") + #print >> logf, string + else: + print (string) + + +############################################################################## +##fz_roots function as used in the IDL code +############################################################################## + +#@profile +def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, + dust_blue=False, disp=False, verbose=False): + + global IGNOREDUST + mscales.setdustcorrect() + raw_lines = {} + raw_lines['[OIII]5007'] = np.array([float('NaN')]) + raw_lines['Hb'] = np.array([float('NaN')]) + raw_lines['Hg'] = np.array([float('NaN')]) + raw_lines['Hz'] = np.array([float('NaN')]) + for k in measured.iterkeys(): + #kills all non-finite terms + measured[k][~(np.isfinite(measured[k][:]))] = 0.0 + + if FIXNEGATIVES: + measured[k][measured[k] < 0] = 0.0 + + if DROPNEGATIVES: + measured[k][measured[k] < 0] = np.nan + + raw_lines[k] = measured[k] + ######we trust QM better than we trust the measurement of the [OIII]4959 + ######which is typical low S/N so we set it to [OIII]5007/3. + ######change this only if youre spectra are very high SNR + raw_lines['[OIII]4959'] = raw_lines['[OIII]5007'] / 3. + raw_lines['[OIII]49595007'] = raw_lines['[OIII]4959'] + raw_lines['[OIII]5007'] + mscales.setHabg(raw_lines['Ha'], raw_lines['Hb'], raw_lines['Hg']) + + #if Ha or Hb is zero, cannot do red correction + if dust_corr and mscales.hasHa and mscales.hasHb and not dust_blue: + with np.errstate(invalid='ignore'): + mscales.calcEB_V() + elif dust_blue and mscales.hasHg and mscales.hasHb: + with np.errstate(invalid='ignore'): + mscales.calcEB_Vblue() + print('----- Using Hg/Hb for E(B-V)_blue estimate. -----') + elif dust_corr and not IGNOREDUST: + + if nps > 1: + print ('''WARNING: reddening correction cannot be done + without both H_alpha and H_beta measurement!!''') + + else: + response = raw_input('''WARNING: reddening correction cannot be done without both H_alpha and H_beta measurement!! + Continuing without reddening correction? [Y/n]\n''').lower() + assert(not (response.startswith('n'))), "please fix the input file to include Ha and Hb measurements" + + IGNOREDUST = True + dust_corr = False + mscales.mds['E(B-V)'] = np.ones(len(raw_lines['Ha'])) * 1e-5 + else: + mscales.unsetdustcorrect() + mscales.mds['E(B-V)'] = np.ones(len(raw_lines['Ha'])) * 1e-5 + + for k in ['[OII]3727', '[OIII]5007', '[OI]6300', '[OIII]4959', + '[SII]6717', '[SII]6731', '[SIII]9069', '[SIII]9532' + '[OII]3727', '[OIII]5007', '[OI]6300', '[OIII]4959', + '[NII]6584', '[SIII]9532']: + if k not in raw_lines or (len(raw_lines[k]) == 1 and np.isnan(raw_lines[k][0])): + raw_lines[k] = np.array([0.] * num) + + mscales.setOlines(raw_lines['[OII]3727'], raw_lines['[OIII]5007'], raw_lines['[OI]6300'], raw_lines['[OIII]4959']) + mscales.setSII(raw_lines['[SII]6717'], raw_lines['[SII]6731'], raw_lines['[SIII]9069'], raw_lines['[SIII]9532']) + mscales.setNII(raw_lines['[NII]6584']) + + if mscales.checkminimumreq(dust_corr, IGNOREDUST) == -1: + return -1 + + mscales.calcNIIOII() + mscales.calcNIISII() + + mscales.calcR23() + #mscales.calcS23() + + mscales.initialguess() + mds = mds.split(',') + + + #mscales.printme() + if verbose: + print ("calculating metallicity diagnostic scales: ", mds) + if 'all' in mds: + mscales.calcD02() + if os.getenv("PYQZ_DIR"): + cmd_folder = os.getenv("PYQZ_DIR") + '/' + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + mscales.calcpyqz() + else: + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable : + export PYQZ_DIR="your/path/where/pyqz/resides/ in bash, for example, if you want this scale. \n''', logf, nps) + + mscales.calcZ94() + mscales.calcM91() + + mscales.calcPP04() + + #mscales.calcP05() + #mscales.calcP10() + + mscales.calcM08() + mscales.calcM13() + + mscales.calcKD02_N2O2() + mscales.calcKK04_N2Ha() + + mscales.calcKK04_R23() + mscales.calcKDcombined() + + mscales.calcD16() + mscales.calcPG16() + mscales.calcC17() + + if 'DP00' in mds: + mscales.calcDP00() + if 'P01' in mds: + mscales.calcP01() + + if 'D02' in mds: + mscales.calcD02() + if 'D13' in mds: + if os.getenv("PYQZ_DIR"): + cmd_folder = os.getenv("PYQZ_DIR") + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + #mscales.calcpyqz() + #in order to see the original pyqz plots + #call pyqz with option plot=True by + #using the commented line below instead + mscales.calcpyqz(plot=disp) + else: + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable + PYQZ_DIR if you want this scale. ''', logf, nps) + + if 'D13all' in mds: + if os.getenv("PYQZ_DIR"): + cmd_folder = os.getenv("PYQZ_DIR") + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + #mscales.calcpyqz() + #in order to see the original pyqz plots + #call pyqz with option plot=True by + #using the commented line below instead + mscales.calcpyqz(plot=disp, allD13=True) + else: + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable + PYQZ_DIR if you want this scale. ''', logf, nps) + + if 'PM14' in mds: + if os.getenv("HIICHI_DIR"): + cmd_folder = os.getenv("HIICHI_DIR") + '/' + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + mscales.calcPM14() + if 'PP04' in mds: + mscales.calcPP04() + if 'Z94' in mds: + mscales.calcZ94() + if 'M91' in mds: + mscales.calcM91() + if 'P10' in mds: + mscales.calcP10() + if 'M13' in mds: + mscales.calcM13() + if 'M08all' in mds: + mscales.calcM08(allM08=True) + elif 'M08' in mds: + mscales.calcM08() + if 'P05' in mds: + mscales.calcP05() + if 'C01' in mds: + mscales.calcC01_ZR23() + if 'KD02' in mds: + mscales.calcKD02_N2O2() + mscales.calcKK04_N2Ha() + mscales.calcKK04_R23() + mscales.calcKDcombined() + + # Modifications by jch [5/2017, Santiago] to include newer scales. + if 'B07_N2O2' in mds: + # Bresolin 2007 N2O2 + mscales.calcB07() + if 'D16' in mds: + # Dopita+ 2016 ONS + mscales.calcD16() + if 'PG16' in mds: + # Pilyugin & Grebel 2016 + mscales.calcPG16() + if 'C17' in mds: + # Curti+ 2017 + mscales.calcC17() + if 'C17all' in mds: + # Curti+ 2017 + mscales.calcC17(allC17=True) + if 'PMC09_N2Ha' in mds: + # Perez-Montero & Contini (2009) + mscales.calcPMC09() + + if 'KK04_N2Ha' in mds: + mscales.calcKD02_N2O2() + mscales.calcKK04_N2Ha() + elif 'KD02_N2O2' in mds: + mscales.calcKD02_N2O2() diff --git a/build/lib/pyMCZ/metscales.py b/build/lib/pyMCZ/metscales.py new file mode 100644 index 0000000..6a57478 --- /dev/null +++ b/build/lib/pyMCZ/metscales.py @@ -0,0 +1,1558 @@ +from __future__ import print_function +import numpy as np +#import sys +import scipy.stats as stats +import numpy.polynomial.polynomial as nppoly +from metallicity import get_keys, printsafemulti + +import pdb + +niter = 5 # number of iteations+1 for KD02 methods + +k_Ha = 2.535 # CCM Rv=3.1 +k_Hb = 3.609 # CCM Rv=3.1 +k_Hg = 4.400 # CCM Rv=3.1 + +#k_O1=2.661 # CCM Rv=3.1 +k_O2 = 4.771 # CCM Rv=3.1 +k_O35007 = 3.341 # CCM Rv=3.1 +k_O34959 = 3.384 # CCM Rv=3.1 +k_O3 = (k_O35007 + k_O34959) / 2. + +k_N2 = 2.443 # CCM Rv=3.1 +k_S2 = 2.381 # CCM Rv=3.1 + +k_S3 = 1 # guess for CCM Rv=3.1 + +global DUSTCORRECT +DUSTCORRECT = True + +''' +R23_coef=np.zeros((5,7)) # coefficients from model grid fits +R23c0=[-3267,-3727.42,-4282.30,-4745.18,-4516.46,-3509.63,-1550.53] +R23_coef[:,0]=[-3267.93,1611.04,-298.187,24.5508,-0.758310] # q=5e6 +R23_coef[:,1]=[-3727.42,1827.45,-336.340,27.5367,-0.845876] # q=1e7 +R23_coef[:,2]=[-4282.30,2090.55,-383.039,31.2159,-0.954473] # q=2e7 +R23_coef[:,3]=[-4745.18,2309.42,-421.778,34.2598,-1.04411] # q=4e7 +R23_coef[:,4]=[-4516.46,2199.09,-401.868,32.6686,-0.996645] # q=8e7 +R23_coef[:,5]=[-3509.63,1718.64,-316.057,25.8717,-0.795242] # q=1.5e8 +R23_coef[:,6]=[-1550.53,784.262,-149.245,12.6618,-0.403774] # q=3e8 + + +N2S2_coef=np.zeros((5,7)) # coefficients from model grid fits +N2S2c0=[-1042.47,-1879.46,-2027.82,-2080.31,-2162.93,-2368.56,-2910.63] +N2S2_coef[:,0]=[-1042.47,521.076,-97.1578,8.00058,-0.245356] +N2S2_coef[:,1]=[-1879.46,918.362,-167.764,13.5700,-0.409872] +N2S2_coef[:,2]=[-2027.82,988.218,-180.097,14.5377,-0.438345] +N2S2_coef[:,3]=[-2080.31,1012.26,-184.215,14.8502,-0.447182] +N2S2_coef[:,4]=[-2162.93,1048.97,-190.260,15.2859,-0.458717] +N2S2_coef[:,5]=[-2368.56,1141.97,-205.908,16.4451,-0.490553] +N2S2_coef[:,6]=[-2910.63,1392.18,-249.012,19.7280,-0.583763] + +O3O2_coef=np.zeros((4,8)) # coefficients from model grid fits +O3O2c0=[-36.9772,-74.2814,-36.7948,-81.1880,-52.6367,-86.8674,-24.4044,49.4728] +O3O2_coef[:,0]=[-36.9772,10.2838,-0.957421,0.0328614] #z=0.05 +O3O2_coef[:,1]=[-74.2814,24.6206,-2.79194,0.110773] # z=0.1 +O3O2_coef[:,2]=[-36.7948,10.0581,-0.914212,0.0300472] # z=0.2 +O3O2_coef[:,3]=[-81.1880,27.5082,-3.19126,0.128252] # z=0.5 +O3O2_coef[:,4]=[-52.6367,16.0880,-1.67443,0.0608004] # z=1.0 +O3O2_coef[:,5]=[-86.8674,28.0455,-3.01747,0.108311] # z=1.5 +O3O2_coef[:,6]=[-24.4044,2.51913,0.452486,-0.0491711] # z=2.0 +O3O2_coef[:,7]=[49.4728,-27.4711,4.50304,-0.232228] # z=3.0 +''' + +M08_coefs = {'R23': [0.7462, -0.7149, -0.9401, -0.6154, -0.2524], + 'N2Ha': [-0.7732, 1.2357, -0.2811, -0.7201, -0.3330], + 'O3Hb': [0.1549, -1.5031, -0.9790, -0.0297], + 'O3O2': [-0.2839, -1.3881, -0.3172], + 'O2Hb': [0.5603, 0.0450, -1.8017, -1.8434, -0.6549], + 'O3N2': [0.4520, -2.6096, -0.7170, 0.1347]} + +C17_coefs = {'N2Ha': [-0.489, 1.513,-2.554,-5.293,-2.867], + 'O3O2': [-0.691,-2.944,-1.308], + 'O3N2': [0.281,-4.765,-2.268], + 'O2Hb': [0.418,-0.961,-3.505,-1.949], + 'O3Hb': [-0.277,-3.549,-3.593,-0.981], + 'R23': [0.527,-1.569,-1.652,-0.421]} + +#this is to check the Maiolino coefficients and find the split maximum of the cirves with degeneracy +''' +import pylab as pl + +x=np.arange(7.0,9.5,0.1) + +for k in M08_coefs.iterkeys(): + print k,max(nppoly.polyval(x-8.69,M08_coefs[k])),x[nppoly.polyval(x-8.69,M08_coefs[k])==max(nppoly.polyval(x-8.69,M08_coefs[k]))] + pl.plot(x,nppoly.polyval(x-8.69,M08_coefs[k]), label=k+' max:%.1f'%x[nppoly.polyval(x-8.69,M08_coefs[k])==max(nppoly.polyval(x-8.69,M08_coefs[k]))]) + +print nppoly.polyval(8.4-8.69,M08_coefs['N2Ha']) +pl.ylabel("log R") +pl.xlabel("12+log(O/H)") +pl.legend(loc=3) +pl.show() +''' + + +class diagnostics: + def __init__(self, num, logf, nps): + self.nm = num + self.Ha = None + self.Hb = None + self.Hg = None + + self.hasHa, self.hasHb, self.hasHg = False, False, False + self.hasO2, self.hasO3 = False, False + self.hasS2, self.hasN2 = False, False + + self.hasO3Hb = False + self.hasO3O2 = False + + self.hasN2O2 = False + self.hasN2S2 = False + + self.hasS26731 = False + self.hasS39532 = False + self.hasS39069 = False + self.hasS2Hb = False + + self.N2O2_roots = None + #other lines calculated and repeatedly used + self.P = None + self.R2 = None + self.R3 = None + self.R23 = None + self.S2Hb = None + self.N2 = None + self.N2S2 = None + self.O23727 = None + self.O35007 = None + self.N26584 = None + self.S26717 = None + self.S26731 = None + self.S39069 = None + self.S39532 = None + + self.O34959p5007 = None + self.O35007O2 = None + self.O2O35007 = None + + self.logR23 = None + self.logN2O2 = None + self.logN2S2 = None + self.logO3O2 = None + self.logS23 = None + #self.logS3S2=None + + self.logO2Hb = None + self.logO3Hb = None + self.logN2Ha = None + self.logS2Ha = None + + self.logO35007O2 = None + self.logO2O35007 = None + + self.logO3O2sq = None + self.logq = None + self.Z_init_guess = None + self.N2O2_coef0 = 1106.8660 + + self.OIII_OII = None + self.OIII_Hb = None + self.OIII_SII = None + + self.NII_OII = None + self.NII_SII = None + #metallicity diagnostics to be returned + + self.mds = {} + for Z in get_keys(): + self.mds[Z] = None + + #setting output file + self.logf = logf + self.nps = nps + + def printme(self, verbose=False): + try: + print ("\nHa", np.mean(self.Ha)) + if verbose: + print (self.Ha) + except (IndexError, TypeError): + pass + try: + print ("\nHb", np.mean(self.Hb)) + if verbose: + print (self.Hb) + except (IndexError, TypeError): + pass + try: + print ("\nO2", np.mean(self.O23727)) + if verbose: + print (self.O23727) + except (IndexError, TypeError): + pass + try: + print ("\nO3", np.mean(self.O35007)) + if verbose: + print (self.O35007) + except (IndexError, TypeError): + pass + try: + print ("\nO34959", np.mean(self.O34959)) + if verbose: + print (self.O34959) + except (IndexError, TypeError): + pass + try: + print ("\nZ94", np.mean(self.mds['Z94'])) + if verbose: + print (self.mds['Z94']) + except (IndexError, TypeError): + pass + try: + print ("\nR23", np.mean(self.R23)) + if verbose: + print (self.R23) + except (IndexError, TypeError): + pass + try: + print ("\nlog(R23)", np.mean(self.logR23)) + if verbose: + print (self.logR23) + except (TypeError, IndexError): + pass + try: + print ("\nlog([NII][OII])", stats.nanmean(self.logN2O2)) + if verbose: + print (self.logN2O2) + except (TypeError, IndexError): + pass + try: + print ("\nlog([OIII][OII])", stats.nanmean(self.logO3O2)) + if verbose: + print (self.logO3O2) + except (TypeError, IndexError): + pass + for k in self.mds.iterkeys(): + print ("\n", k) + try: + print (stats.nanmean(self.mds[k]), np.stdev(self.mds[k])) + except (IndexError, TypeError): + if verbose: + print (self.mds[k]) + + def checkminimumreq(self, red_corr, ignoredust): + if red_corr and not ignoredust: + if not self.hasHa: + return -1 + if not self.hasHb: + return -1 + #if not self.hasO2 : + # return -1 + #if not self.hasO3 : + # return -1 + #if not self.hasS2 : + # return -1 + if not self.hasN2 and not (self.hasO2 and self.hasO3): + return -1 + + def fz_roots(self, coef): + if len(coef.shape) == 1: + coef[~(np.isfinite(coef))] = 0.0 + rts = np.roots(coef[::-1]) + if rts.size == 0: + printsafemulti('WARNING: fz_roots failed', self.logf, self.nps) + rts = np.zeros(coef.size - 1) + return rts + + else: + rts = np.zeros((coef.shape[0], coef.shape[1] - 1), dtype=complex) + coef[~(np.isfinite(coef))] = 0.0 + + for i in range(coef.shape[0]): + rts[i] = np.roots(coef[i][::-1]) # ::-1][0]) + return rts + + def setdustcorrect(self): + global DUSTCORRECT + DUSTCORRECT = True + + def unsetdustcorrect(self): + global DUSTCORRECT + DUSTCORRECT = False + + def dustcorrect(self, l1, l2, flux=False): + #global DUSTCORRECT + if DUSTCORRECT: + if not flux: + return 0.4 * self.mds['E(B-V)'] * (l1 - l2) + return 10 ** (0.4 * self.mds['E(B-V)'] * (l1 - l2)) + else: + if not flux: + return 0 + return 1.0 + + def setHabg(self, Ha, Hb, Hg): + self.Ha = Ha + self.Hb = Hb + self.Hg = Hg + if sum(self.Ha > 0): + self.hasHa = True + if sum(self.Hb > 0): + self.hasHb = True + if sum(self.Hg > 0): + self.hasHg = True + + def setOlines(self, O23727, O35007, O16300, O34959): + self.O23727 = O23727 + self.O35007 = O35007 + self.O16300 = O16300 + + if sum(self.O35007 > 0): + self.hasO3 = True + if sum(self.O23727 > 0): + self.hasO2 = True + + if self.hasO2 and self.hasO3: + self.O35007O2 = (self.O35007 / self.O23727) * self.dustcorrect(k_O3, k_O2, flux=True) + self.O2O35007 = (self.O23727 / self.O35007) * self.dustcorrect(k_O2, k_O3, flux=True) + + self.logO35007O2 = np.log10(self.O35007O2) + self.logO2O35007 = np.log10(self.O2O35007) + + #self.logO2O35007Hb=np.log10((self.O23727+self.O35007)/self.Hb) + # ratios for other diagnostics - slightly different ratios needed + # commenting since it is not used + #if self.hasHb: + # self.logO2O35007Hb = np.log10( + # (self.O23727 / self.Hb) * self.dustcorrect(k_O2, + # k_Hb, flux=True) + # + (self.O35007 / self.Hb) * self.dustcorrect(k_O35007, + # k_Hb, flux=True)) + + else: + printsafemulti("WARNING: needs O lines and and Ha/b: did you run setHab()?", self.logf, self.nps) + if self.hasHb: + if self.hasO2: + self.logO2Hb = np.log10(self.O23727 / self.Hb) + \ + self.dustcorrect(k_O2, k_Hb, flux=False) + # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + if self.hasO3: + self.O3Hb = (self.O35007 / self.Hb) * \ + self.dustcorrect(k_O35007, k_Hb, flux=True) + # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + self.logO3Hb = np.log10(self.O3Hb) + self.hasO3Hb = True + + + if self.hasO2 and self.hasO3: + self.OIII_OII = np.log10(self.O35007 / self.O23727 * self.dustcorrect(k_O35007, k_O2, flux=True)) + if O34959 is not None and sum(O34959 > 0) > 0: + self.O34959p5007 = (O34959 + self.O35007) + self.logO3O2 = np.log10((self.O34959p5007) / self.O23727)\ + + self.dustcorrect(k_O3, k_O2, flux=False) + #this is useful when we get logq + self.hasO3O2 = True + if self.hasHb: + self.OIII_Hb = np.log10(self.O35007 / self.Hb * self.dustcorrect(k_O35007, k_Hb, flux=True)) + + def setNII(self, N26584): + if N26584 is not None and sum(N26584 > 0): + self.N26584 = N26584 + self.hasN2 = True + if self.hasHa: + self.logN2Ha = np.log10(self.N26584 / self.Ha) # +self.dustcorrect(k_N2,k_Ha,flux=True) + #lines are very close: no dust correction + #Note: no dust correction cause the lies are really close! + else: + printsafemulti("WARNING: needs NII6584 and Ha to calculate NIIHa: did you run setHab()?", self.logf, self.nps) + if self.hasS2 and self.hasS26731 and self.hasN2: + self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_S2,flux=True) + #lines are very close: no dust correction + if self.hasO2 and self.hasN2: + self.NII_OII = np.log10(self.N26584 / self.O23727 * self.dustcorrect(k_N2, k_O2, flux=True)) + + def setSII(self, S26717, S26731, S39069, S39532): + if S26717 is not None and sum(S26717 > 0) > 0: + self.S26717 = S26717 + self.hasS2 = True + + if self.hasHa: + self.logS2Ha = np.log10(self.S26717 / self.Ha) + self.dustcorrect(k_S2, k_Ha, flux=False) + else: + printsafemulti("WARNING: needs SII6717 and Ha to calculate SIIHa: did you run setHab() and setS()?", self.logf, self.nps) + if S26731 is not None and sum(S26731 > 1e-9) > 0: + self.S26731 = S26731 + self.hasS26731 = True + if S39069 is not None and sum(S39069 > 1e-9) > 0: + self.S39069 = S39069 + self.hasS39069 = True + if S39532 is not None and sum(S39532 > 1e-9) > 0: + self.S39532 = S39532 + self.hasS39532 = True + if self.hasS2: + if self.hasN2 and self.NII_SII is None and self.hasS26731: + self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_O2,flux=True) + #lines are very close: no dust correction + if self.hasO3 and self.OIII_SII is None and self.hasS26731: + self.OIII_SII = np.log10(self.O35007 / (self.S26717 + self.S26731) * self.dustcorrect(k_O3, k_S2, flux=True)) + + #@profile + def calcEB_V(self): + printsafemulti("calculating E(B-V)", self.logf, self.nps) + a2b_intrinsic = 2.86 + self.mds['E(B-V)'] = np.log10(a2b_intrinsic * self.Hb / self.Ha) / (0.4 * (k_Ha - k_Hb)) # E(B-V) + self.mds['E(B-V)'][self.mds['E(B-V)'] <= 0] = 1e-5 + + #@profile + def calcEB_Vblue(self): + printsafemulti("calculating E(B-V)_blue", self.logf, self.nps) + b2g_intrinsic = 2.145 + self.mds['E(B-V)'] = np.log10(b2g_intrinsic * self.Hg / self.Hb) / (0.4 * (k_Hb - k_Hg)) # E(B-V) + self.mds['E(B-V)'][self.mds['E(B-V)'] <= 0] = 1e-5 + + #@profile + def calcNIISII(self): + if self.hasS2 and self.hasN2: + self.N2S2 = self.N26584 / self.S26717 * self.dustcorrect(k_N2, k_S2, flux=True) # 0.4*self.mds['E(B-V)']*(k_N2-k_S2) + self.logN2S2 = np.log10(self.N26584 / self.S26717) + self.dustcorrect(k_N2, k_S2, flux=False) # 0.4*self.mds['E(B-V)']*(k_N2-k_S2) + self.hasN2S2 = True + else: + printsafemulti("WARNING: needs SII6717 and NII6584 to calculate NIISII: did you run setN2() and setS?", self.logf, self.nps) + + #@profile + def calcNIIOII(self): + if self.hasN2 and self.hasO2: + self.logN2O2 = np.log10(self.N26584 / self.O23727) + self.dustcorrect(k_N2, k_O2, flux=False) + + self.hasN2O2 = True + if not self.hasN2O2 or np.mean(self.logN2O2) < -1.2: + + try: + printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods should only be used for log([NII]6564/[OII]3727) > -1.2, + the mean log([NII]6564/[OII]3727)= %f''' % np.mean(self.logN2O2), self.logf, self.nps) + except TypeError: + printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods + should only be used for log([NII]6564/[OII]3727) >-1.2, + the mean log([NII]6564/[OII]3727)= %s''' % self.logN2O2, self.logf, self.nps) + + if not self.hasN2O2: + self.N2O2_roots = np.zeros(self.nm) + float('NaN') + else: + N2O2_coef = np.array([[self.N2O2_coef0, -532.15451, 96.373260, -7.8106123, 0.23928247]] * self.nm).T # q=2e7 line (approx average) + N2O2_coef[0] -= self.logN2O2 + N2O2_coef = N2O2_coef.T + # finding roots for == (4) + self.N2O2_roots = np.array([self.fz_roots(N2O2_coef)])[0] + + #@profile + def calcR23(self): + printsafemulti("calculating R23", self.logf, self.nps) + + #R23 NEW Comb, [NII]/Ha: KK04 = Kobulnicky & Kewley, 2004, submitted' + if self.hasO3 and self.hasO2 and self.hasHb: + self.R2 = (self.O23727 / self.Hb) * self.dustcorrect(k_O2, k_Hb, flux=True) + self.R3 = (self.O34959p5007 / self.Hb) * self.dustcorrect(k_O3, k_Hb, flux=True) + self.R23 = self.R2 + self.R3 + self.logR23 = np.log10(self.R23) + self.mds['logR23'] = self.logR23 + #note that values of logR23 > 0.95 are unphysical. + #you may choose to uncomment the line below + #self.logR23[self.logR23>0.95]=0.95 + else: + printsafemulti("WARNING: need O3, O2, Hb", self.logf, self.nps) + + #@profile + def calcS23(self): + printsafemulti("calculating S23", self.logf, self.nps) + #the original code here uses S267176731, + #which is however set to 6717 as default + #Vilchez & Esteban (1996) + if self.hasS2: + if self.hasS39069 and self.hasHb: + self.logS23 = np.log10((self.S26717 / self.Hb) * + self.dustcorrect(k_S2, k_Hb, flux=True) + + (self.S39069 / self.Hb) * + self.dustcorrect(k_S3, k_Hb, flux=True)) + + #self.logS3S2=np.log10(S39069/self.S26717)+self.dustcorrect(k_S3,k_S2) + + ##@profile + def calclogq(self, Z): + if not self.hasO3O2: + printsafemulti("WARNING: needs O3,O2,Hb to calculate logq properly.", self.logf, self.nps) + return -1 + if self.logO3O2sq is None: + self.logO3O2sq = self.logO3O2 ** 2 + return (32.81 - 1.153 * self.logO3O2sq + Z * (-3.396 - 0.025 * self.logO3O2 + 0.1444 * self.logO3O2sq)) / (4.603 - 0.3119 * self.logO3O2 - \ + 0.163 * self.logO3O2sq + Z * (-0.48 + 0.0271 * self.logO3O2 + 0.02037 * self.logO3O2sq)) + + ##@profile + def initialguess(self): + # Initial Guess - appearing in LK code as of Nov 2006 + # upper branch: if no lines are available, metallicity is set to 8.7 + self.Z_init_guess = np.zeros(self.nm) + 8.7 + # use [N2]/Ha + if self.hasHa and self.hasN2: + self.Z_init_guess[(self.logN2Ha < -1.3) & (self.N26584 != 0.0)] = 8.2 + self.Z_init_guess[(self.logN2Ha < -1.1) & (self.logN2Ha >= -1.3) & (self.N26584 != 0.0)] = 8.4 + #A1 KE08 + self.Z_init_guess[(self.logN2Ha >= -1.1) & (self.N26584 != 0.0)] = 8.7 + #use [N2]/[O2] + if self.hasN2 and self.hasO2: + N2O2 = np.zeros(self.nm) + float('nan') + if self.hasHb: + ###FED CHECK THIS! + N2O2 = self.N26584 * self.Ha * self.Hb * self.O23727 + if not self.hasN2O2: + printsafemulti("WARNING: must calculate logN2O2 first", self.logf, self.nps) + self.calcNIIOII() + self.Z_init_guess[(self.logN2O2 < -1.2) & (N2O2 != 0.0)] = 8.2 + # at logN2O2<-1.2 using low-Z gals, A1 KE08 + self.Z_init_guess[(self.logN2O2 >= -1.2) & (N2O2 != 0.0)] = 8.7 + + # at logN2O2>-1.2 using HII regions + + + + #######################these are the metallicity diagnostics################## + #@profile + def calcpyqz(self, plot=False, allD13=False): + printsafemulti("calculating D13", self.logf, self.nps) + + # initializing variable pyqz to avoid style issues + # (pyqz not defined is reported as error by Landscape.io w/ import in func + pyqz = None + try: + import pyqz + except ImportError: + return -1 + + #check version of pyqz + from distutils.version import StrictVersion + oldpyqz = False + if StrictVersion(pyqz.__version__) <= StrictVersion('0.5.0'): + oldpyqz = True + + if self.NII_SII is not None and allD13: + if self.OIII_SII is not None: + + if oldpyqz: + self.mds['D13_N2S2_O3S2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_SII]), 'NII/SII', 'OIII/SII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + #pyqz.get_grid_fn(Pk=5.0,calibs='GCZO', kappa =20, struct='pp') + # self.mds['D13_N2S2_O3S2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + # np.atleast_1d([self.OIII_SII])], \ + # '[NII]/[SII]+;[OIII]/[SII]+', \ + # show_plot=plot, n_plot=False, \ + # save_plot=False, verbose=False)[0].T + self.mds['D13_N2S2_O3S2'] = pyqz.interp_qz('Tot[O]+12', + [np.atleast_1d([self.NII_SII]), + np.atleast_1d([self.OIII_SII])], + '[NII]/[SII]+;[OIII]/[SII]+')[0].T + + if self.OIII_Hb is not None: + if oldpyqz: + self.mds['D13_N2S2_O3Hb'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_Hb]), 'NII/SII', 'OIII/Hb', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + # self.mds['D13_N2S2_O3SHb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + # np.atleast_1d([self.OIII_Hb])], \ + # '[NII]/[SII]+;[OIII]/Hb', \ + # show_plot=plot, n_plot=False, \ + # save_plot=False, verbose=False)[0].T + self.mds['D13_N2S2_O3SHb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/[SII]+;[OIII]/Hb')[0].T + + + + if self.OIII_OII is not None: + if oldpyqz: + self.mds['D13_N2S2_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_OII]), 'NII/SII', 'OIII/OII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2S2_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_OII])], \ + '[NII]/[SII]+;[OIII]/[OII]+', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + + if self.NII_OII is not None and allD13: + if self.OIII_SII is not None: + if oldpyqz: + self.mds['D13_N2O2_O3S2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_SII]), 'NII/OII', 'OIII/SII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2O2_O3S2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_SII])], \ + '[NII]/[OII]+;[OIII]/[SII]+', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + + if self.OIII_Hb is not None: + if oldpyqz: + self.mds['D13_N2O2_O3Hb'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_Hb]), 'NII/OII', 'OIII/Hb', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2O2_O3Hb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/[OII]+;[OIII]/Hb')[0].T + + if self.OIII_OII is not None: + if oldpyqz: + self.mds['D13_N2O2_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_OII]), 'NII/OII', 'OIII/OII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2O2_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_OII])], \ + '[NII]/[OII]+;[OIII]/[OII]+')[0].T + if self.logN2Ha is not None: + if self.OIII_Hb is not None: + if oldpyqz: + self.mds['D13_N2Ha_O3Hb'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_Hb]), 'NII/Ha', 'OIII/Hb', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2Ha_O3Hb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/Ha;[OIII]/Hb')[0].T + if self.OIII_OII is not None: + if oldpyqz: + self.mds['D13_N2Ha_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_OII]), 'NII/Ha', 'OIII/OII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + + else: + self.mds['D13_N2Ha_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/Ha;[OIII]/[OII]+')[0].T + + #@profile + def calcDP00(self): + # Diaz, A. I., & Perez-Montero, E. 2000, MNRAS, 312, 130 + # As per KD02: DP00 diagnostic systematically underestimates the + # abundance relative to the comparison abundance. + # A term is added to improve the fit according to KD02 Eq. 6 + # AVAILABLE BUT DEPRECATED + printsafemulti("calculating DP00", self.logf, self.nps) + + if self.logS23 is None: + self.calcS23() + if self.logS23 is None: + printsafemulti("WARNING: Cannot compute this without S23", self.logf, self.nps) + return -1 + self.mds['DP00'] = 1.53 * self.logS23 + 8.27 + 1.0 / (2.0 - 9.0 * self.logS23 ** 3) + + #@profile + def calcD02(self): + # [NII]/Ha Denicolo, Terlevich & Terlevich (2002), MNRAS, 330, 69 + #FED:added uncertainties + printsafemulti("calculating D02", self.logf, self.nps) + + e1 = np.random.normal(0, 0.05, self.nm) + e2 = np.random.normal(0, 0.1, self.nm) + if self.hasN2 and self.hasHa: + self.mds['D02'] = 9.12 + e1 + (0.73 + e2) * self.logN2Ha + else: + printsafemulti("WARNING: need N2Ha to do this. " + + "did you run setHab and setNII", self.logf, self.nps) + + #@profile + def calcPP04(self): + ### PP04_N2_Z, PP04_O3N2_Z Pettini & Pagel diagnostics - + ### Pettini & Pagel (2004), MNRAS, 348, L59 + # [NII]/Ha Pettini & Pagel (2004), MNRAS, 348, L59 + #discriminating lower and upper branch using [NII]/[OII] or [NII]/Ha + printsafemulti("calculating PP04", self.logf, self.nps) + if self.hasN2 and self.hasHa: + self.mds['PP04_N2Ha'] = nppoly.polyval(self.logN2Ha, [9.37, 2.03, 1.26, 0.32]) + + #FED: restricting the range as per paper + index = (self.logN2Ha > -2.5) * (self.logN2Ha < -0.3) + self.mds['PP04_N2Ha'][~index] = float('NaN') + if self.hasO3Hb: + self.mds['PP04_O3N2'] = 8.73 - 0.32 * (self.logO3Hb - self.logN2Ha) + index = (self.logO3Hb > 2) + self.mds['PP04_O3N2'][index] = float('NaN') + else: + printsafemulti("WARNING: need O3Hb for PP04_O3N2", self.logf, self.nps) + else: + printsafemulti("WARNING: need N2Ha to do this. did you run setHab and setNII", self.logf, self.nps) + + #@profile + def calcZ94(self): + ### calculating z from Kobulnicky,Kennicutt,Pizagno (1998) + ### parameterization of Zaritzky et al. (1994) + ###Z94 = Zaritsky, D., Kennicutt, R. C., & Huchra, J. P. 1994, + ###ApJ, 420, 87 + ### only valid on the upper branch of R23 (KE08 A2.4) + + printsafemulti("calculating Z94", self.logf, self.nps) + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + self.mds['Z94'] = nppoly.polyval(self.logR23, [9.265, -0.33, -0.202, -0.207, -0.333]) + self.mds['Z94'][(self.logR23 > 0.9)] = None + ## 0.9 is a conservative constraint to make sure that we are + ## only using the upper branch (i.e. 12+log(O/H)>8.4) + + def calcP(self): + if self.P is None: + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + #R3=10**self.logO349595007Hb + #R2=10**self.logO2Hb + #P = R3/(R2+R3) + self.P = self.R3 / self.R23 + + #@profile + def calcP05(self): + # #### P-method ##### + ##Pilyugin+ 2005 method. Based on [OIII],[OII], Hbeta + ##calibrated from Te method + # make sure you run setOlines() first + printsafemulti("calculating P05", self.logf, self.nps) + + if self.calcP() == -1: + return -1 + if self.Z_init_guess is None: + self.initialguess() + + Psq = self.P * self.P + + P_abund_up = (self.R23 + 726.1 + 842.2 * self.P + 337.5 * Psq) / (85.96 + 82.76 * self.P + 43.98 * Psq + 1.793 * self.R23) + P_abund_low = (self.R23 + 106.4 + 106.8 * self.P - 3.40 * Psq) / (17.72 + 6.60 * self.P + 6.95 * Psq - 0.302 * self.R23) + + self.mds['P05'] = P_abund_up + self.mds['P05'][self.Z_init_guess < 8.4] = P_abund_low[self.Z_init_guess < 8.4] + + #@profile + def calcP10(self): + # #### P-method ##### + ##Pilyugin+ 2010 method. + ##calibrated from Te method + # need Hb + #The Astrophysical Journal, Volume 720, Issue 2, pp. 1738-1751 (2010). + #Published in Sep 2010 + + printsafemulti("calculating P10", self.logf, self.nps) + + if not self.hasHb: + printsafemulti("this method needs Hb", self.logf, self.nps) + return -1 + self.mds['P10_ONS'] = np.zeros(self.nm) + float('NaN') + self.mds['P10_ON'] = np.zeros(self.nm) + float('NaN') + #P10N2=np.zeros(self.nm)+float('NaN') + #P10S2=np.zeros(self.nm)+float('NaN') + P10logR3 = np.zeros(self.nm) + float('NaN') + P10logR2 = np.zeros(self.nm) + float('NaN') + P10logN2 = np.zeros(self.nm) + float('NaN') + P10logS2 = np.zeros(self.nm) + float('NaN') + + self.calcP() + if self.R2 is not None: + P10logR2 = np.log(self.R2) + + if self.R3 is not None: + P10logR3 = np.log(self.R3) + + if self.hasN2: + #the ratio of N26548 and N26548 is N26584/N26548 = 3 + #independent on physical conditions + #The Physics and Dynamics of Planetary Nebulae + # By Grigor A. Gurzadyan + P10logN2 = (np.log((self.N26584 * 1.33) / self.Hb) + + self.dustcorrect(k_N2, k_Hb, flux=False)) + + if self.hasS2 and self.hasS26731: + self.S2Hb = ((self.S26717 + self.S26731) / self.Hb + * self.dustcorrect(k_S2, k_Hb, flux=True)) + self.hasS2Hb = True + P10logS2 = np.log10(self.S2Hb) + + P10logN2S2 = P10logN2 - P10logS2 + P10logN2R2 = P10logN2 - P10logR2 + P10logS2R2 = P10logS2 - P10logR2 + + coefsONS0 = np.array([8.277, 0.657, -0.399, -0.061, 0.005]) + coefsONS1 = np.array([8.816, -0.733, 0.454, 0.710, -0.337]) + coefsONS2 = np.array([8.774, -1.855, 1.517, 0.304, 0.328]) + + vsONS = np.array([np.ones(self.nm), self.P, P10logR3, P10logN2R2, P10logS2R2]).T + + coefsON0 = np.array([8.606, -0.105, -0.410, -0.150]) + coefsON1 = np.array([8.642, 0.077, 0.411, 0.601]) + coefsON2 = np.array([8.013, 0.905, 0.602, 0.751]) + + vsON = np.array([np.ones(self.nm), P10logR3, P10logR2, P10logN2R2]).T + + indx = P10logN2 > -0.1 + if self.P is not None: + self.mds['P10_ONS'][indx] = np.dot(vsONS[indx], coefsONS0) + self.mds['P10_ON'][indx] = np.dot(vsON[indx], coefsON0) + + indx = (P10logN2 < -0.1) * (P10logN2S2 > -0.25) + if self.P is not None: + self.mds['P10_ONS'][indx] = np.dot(vsONS[indx], coefsONS1) + self.mds['P10_ON'][indx] = np.dot(vsON[indx], coefsON1) + + indx = (P10logN2 < -0.1) * (P10logN2S2 < -0.25) + if self.P is not None: + self.mds['P10_ONS'][indx] = np.dot(vsONS[indx], coefsONS2) + self.mds['P10_ON'][indx] = np.dot(vsON[indx], coefsON2) + + indx = ~((self.mds['P10_ONS'] > 7.1) * (self.mds['P10_ON'] > 7.1) * (self.mds['P10_ONS'] < 9.4) * (self.mds['P10_ON'] < 9.4)) + if self.P is not None: + self.mds['P10_ONS'][indx] = float('NaN') + self.mds['P10_ON'][indx] = float('NaN') + + #@profile + def calcP01(self): + # P-method 2001 upper branch (derecated and commented out) + # Pilyugin 2001 + # available but deprecated + printsafemulti("calculating old P05", self.logf, self.nps) + + if self.Z_init_guess is None: + self.initialguess() + if self.hasO3O2 and self.hasO3 and self.hasO2: + P = 10 ** self.logO3O2 / (1 + 10 ** self.logO3O2) + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + Psq = P ** 2 + P_abund_old = (self.R23 + 54.2 + 59.45 * P + 7.31 * Psq) / (6.07 + 6.71 * P + 0.371 * Psq + 0.243 * self.R23) + self.mds['P01'] = np.zeros(self.nm) + float('NaN') + self.mds['P01'][self.Z_init_guess >= 8.4] = P_abund_old[self.Z_init_guess >= 8.4] + else: + printsafemulti("WARNING: need OIIIOII to calculate P01, did you set them up with setOlines()?", self.logf, self.nps) + + #@profile + def calcC01_ZR23(self): + # C01 = Charlot, S., & Longhetti, M., 2001, MNRAS, 323, 887 + # Charlot 01 R23 calibration: (case F) ## + # available but deprecated + printsafemulti("calculating C01", self.logf, self.nps) + + if self.hasO3 and self.hasO2 and self.hasO3Hb: + x2 = self.O2O35007 / 1.5 + x3 = (10 ** self.logO3Hb) * 0.5 + self.mds['C01_R23'] = np.zeros(self.nm) + float('NaN') + self.mds['C01_R23'][self.O2O35007 < 0.8] = np.log10(3.78e-4 * (x2[self.O2O35007 < 0.8]) ** 0.17 * x3[self.O2O35007 < 0.8] ** (-0.44)) + 12.0 + + self.mds['C01_R23'][self.O2O35007 >= 0.8] = np.log10(3.96e-4 * x3[self.O2O35007 >= 0.8] ** (-0.46)) + 12.0 + else: + printsafemulti('''WARNING: need [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, +did you set them up with setOlines()?''', self.logf, self.nps) + + # Charlot 01 calibration: (case A) based on [N2]/[SII]## + # available but deprecated + if not self.hasN2S2: + printsafemulti("WARNING: trying to calculate logNIISII", self.logf, self.nps) + self.calcNIISII() + if self.hasN2S2 and self.hasO3 and self.hasO2 and self.hasO3Hb: + self.mds['C01_N2S2'] = np.log10(5.09e-4 * (x2 ** 0.17) * ((self.N2S2 / 0.85) ** 1.17)) + 12 + else: + printsafemulti('''WARNING: needs [NII]6584, [SII]6717, [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, +did you set them up with setOlines() and ?''', self.logf, self.nps) + + #@profile + def calcM91(self): + # ## calculating McGaugh (1991) + # McGaugh, S.S., 1991, ApJ, 380, 140' + # M91 calibration using [N2O2] as + # initial estimate of abundance: + # this initial estimate can be + # changed by replacing + # OH_init by another guess, eg C01_Z + # NOTE: occasionally the M91 + # 'upper branch' will give a metallicity + # that is lower than the 'lower branch'. + # Happens for very high R23 values. + # If R23 is higher than the intersection + # (calculate the intersection), then + # the metallicity is likely to be around + # the R23 maximum = 8.4 + + printsafemulti("calculating M91", self.logf, self.nps) + self.mds['M91'] = np.zeros(self.nm) + float('NaN') + + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + + if self.Z_init_guess is None: + self.initialguess() + + M91_Z_low = nppoly.polyval(self.logR23, [12.0 - 4.944, 0.767, 0.602]) - \ + self.logO3O2 * nppoly.polyval(self.logR23, [0.29, 0.332, -0.331]) + M91_Z_up = nppoly.polyval(self.logR23, [12.0 - 2.939, -0.2, -0.237, -0.305, -0.0283]) - \ + self.logO3O2 * nppoly.polyval(self.logR23, [0.0047, -0.0221, -0.102, -0.0817, -0.00717]) + + indx = (np.abs(self.logO3O2) > 0) * (np.abs(self.logR23) > 0) * (self.Z_init_guess < 8.4) + self.mds['M91'][indx] = M91_Z_low[indx] + indx = (np.abs(self.logO3O2) > 0) * (np.abs(self.logR23) > 0) * (self.Z_init_guess >= 8.4) + self.mds['M91'][indx] = M91_Z_up[indx] + self.mds['M91'][(M91_Z_up < M91_Z_low)] = float('NaN') + + #@profile + def calcM13(self): + #Marino+ 2013 + printsafemulti("calculating M13", self.logf, self.nps) + + if not self.hasHa or not self.hasN2: + printsafemulti("WARNING: need O3, N2, Ha and Hb, " + + "or at least N2 and Ha", self.logf, self.nps) + return -1 + else: + e1 = np.random.normal(0, 0.027, self.nm) + e2 = np.random.normal(0, 0.024, self.nm) + self.mds["M13_N2Ha"] = 8.743 + e1 + (0.462 + e2) * self.logN2Ha + if self.hasHb and self.hasO3: + e1 = np.random.normal(0, 0.012, self.nm) + e2 = np.random.normal(0, 0.012, self.nm) + O3N2 = self.logO3Hb - self.logN2Ha + self.mds["M13_O3N2"] = 8.533 + e1 - (0.214 + e1) * O3N2 + index = (O3N2 > 1.7) + self.mds["M13_O3N2"][index] = float('NaN') + index = (O3N2 < -1.1) + self.mds["M13_O3N2"][index] = float('NaN') + + #for i in range(len(self.mds["M13_O3N2"])): + #print ("here O3N2",#self.O35007[i], self.Hb[i], + #self.O3Hb[i], + #self.logO3Hb[i], self.logN2Ha[i], + # O3N2[i], + # self.mds["M13_O3N2"][i]) + + #@profile + def calcM08(self, allM08=False): + #Maiolino+ 2008 + #Astronomy and Astrophysics, Volume 488, Issue 2, 2008, pp.463-479 + #Published in Sep 2008 + printsafemulti("calculating M08", self.logf, self.nps) + highZ = None + if self.logO35007O2 is not None: + self.mds['M08_O3O2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O3O2']] * self.nm).T + coefs[0] = coefs[0] - self.logO35007O2 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + #the two cumsum assure that if the condition for the ith element + #of indx is [False, False] then after the first cumsum(1) is [0,0] + #[False, True] is [0,1] + #[True, True] is [1,2] + #but (here is the kicker) [True, False] is [1,1]. + #Because i want only one solution + #(i'll settle for the first one occurring) [1,1] is ambiguous. + #The second cumsum(1) makes + #[0,0]->[0,0], [0,1]->[0,1], [1,2]->[1,3] and finally [1,1]->[1,2] + + self.mds['M08_O3O2'][(indx.sum(1)) > 0] = sols[indx].real + highZ = np.median(self.logO35007O2) < 0 + if self.logN2Ha is not None: + self.mds['M08_N2Ha'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['N2Ha']] * self.nm).T + coefs[0] = coefs[0] - self.logN2Ha + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_N2Ha'][(indx.sum(1)) > 0] = sols[indx].real + if highZ is None: + highZ = np.median(self.logN2Ha) > -1.3 + + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute M08_R23 without R23", self.logf, self.nps) + else: + self.mds['M08_R23'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['R23']] * self.nm).T + coefs[0] = coefs[0] - self.logR23 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real >= 8.0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_R23'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real <= 8.0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_R23'][(indx.sum(1)) > 0] = sols[indx].real + if not allM08: + return + else: + printsafemulti("calculating other M08s", self.logf, self.nps) + + if self.logO3Hb is not None: + + self.mds['M08_O3Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O3Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO3Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real >= 7.9)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O3Hb'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real <= 7.9)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O3Hb'][(indx.sum(1)) > 0] = sols[indx].real + + if self.logO2Hb is not None: + self.mds['M08_O2Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O2Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO2Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real >= 8.7)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real <= 8.7)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + + + + if self.hasO3 and self.hasN2: + self.mds['M08_O3N2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O3N2']] * self.nm).T + coefs[0] = coefs[0] - np.log10(self.O35007 / self.N26584) \ + + self.dustcorrect(k_O35007, k_N2, flux=False) + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= 7.1) * (sols.real <= 9.4)\ + * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O3N2'][(indx.sum(1)) > 0] = sols[indx].real + + #@profile + def calcKD02_N2O2(self): + ## Kewley & Dopita (2002) estimates of abundance + ## KD02 + # KD02 [N2]/[O2] estimate (can be used for whole log(O/H)+12 range, + # but rms scatter increases to 0.11 rms for log(O/H)+12 < 8.6 + # rms = 0.04 for + # log(O/H)+12 > 8.6 + # uses equation (4) from KD02 paper + # FED: i vectorized the hell out of this function!!! + # from a 7 dimensional if/for loop to 1 if and 1 for :-D + #vectorizing makes fed happy ... + + printsafemulti("calculating KD02_N2O2", self.logf, self.nps) + + if self.hasN2 and self.hasO2 and self.hasHa and self.hasHb: + self.mds['KD02_N2O2'] = np.zeros(self.nm) + float('NaN') + if not self.hasN2O2: + printsafemulti("WARNING: must calculate logN2O2 first", self.logf, self.nps) + self.calcNIIOII() + if not self.hasN2O2 or self.N2O2_roots is None or sum(np.isnan(self.N2O2_roots.flatten())) == len(self.N2O2_roots.flatten()): + printsafemulti("WARNING: cannot calculate N2O2", self.logf, self.nps) + return -1 + roots = self.N2O2_roots.T + for k in range(4): + indx = (abs(roots[k]) >= 7.5) * (abs(roots[k]) <= 9.4) * (roots[k][:].imag == 0.0) + self.mds['KD02_N2O2'][indx] = abs(roots[k][indx]) + else: + printsafemulti("WARNING: need NII6584 and OII3727 and Ha and Hb to calculate this. did you run setO() setHab() and setNII()?", self.logf, self.nps) + return 1 + + #@profile + def calcKK04_N2Ha(self): + # calculating [N2]/Ha abundance estimates using [O3]/[O2] also + printsafemulti("calculating KK04_N2Ha", self.logf, self.nps) + + if self.mds['KD02_N2O2'] is None: + self.calcKD02_N2O2() + if self.mds['KD02_N2O2'] is None or sum(np.isnan(self.mds['KD02_N2O2'])) == self.nm: + printsafemulti("WARNING: without KD02_N2O2 cannot calculate KK04_N2Ha properly, but we will do our best...", self.logf, self.nps) + Z_new_N2Ha = np.zeros(self.nm) + 8.6 + else: + Z_new_N2Ha = self.mds['KD02_N2O2'].copy() # was 8.6 + + if self.hasN2 and self.hasHa: + logq_save = np.zeros(self.nm) + convergence, tol, ii = 100, 1.0e-3, 0 + if self.hasO3O2: + # calculating logq using the [N2]/[O2] + # metallicities for comparison + while convergence > tol and ii < 100: + ii += 1 + self.logq = self.calclogq(Z_new_N2Ha) + Z_new_N2Ha = nppoly.polyval(self.logN2Ha, [7.04, 5.28, 6.28, 2.37]) - \ + self.logq * nppoly.polyval(self.logN2Ha, [-2.44, -2.01, -0.325, +0.128]) + \ + 10 ** (self.logN2Ha - 0.2) * self.logq * (-3.16 + 4.65 * self.logN2Ha) + convergence = np.abs(self.logq - logq_save).mean() + logq_save = self.logq.copy() + if ii >= 100: + printsafemulti("WARNING [KK04_N2Ha]: loop did not converge", self.logf, self.nps) + Z_new_N2Ha = np.zeros(self.nm) + float('NaN') + else: + self.logq = 7.37177 * np.ones(self.nm) + Z_new_N2Ha = nppoly.polyval(self.logN2Ha, [7.04, 5.28, 6.28, 2.37]) - \ + self.logq * nppoly.polyval(self.logN2Ha, [-2.44, -2.01, -0.325, +0.128]) + \ + 10 ** (self.logN2Ha - 0.2) * self.logq * (-3.16 + 4.65 * self.logN2Ha) + self.mds['KK04_N2Ha'] = Z_new_N2Ha + indx = self.logN2Ha > 0.8 + self.mds['KK04_N2Ha'][indx] = float('NaN') + else: + printsafemulti("WARNING: need NII6584 and Ha to calculate this. did you run setHab() and setNII()?", self.logf, self.nps) + + #@profile + def calcKK04_R23(self): + # Kobulnicky & Kewley 2004 + # calculating upper and lower metallicities for objects without + # Hb and for objects without O3 and/or O2 + + printsafemulti("calculating KK04_R23", self.logf, self.nps) + #this is in the original code but not used :( + #if self.hasN2 and self.hasHa: + #logq_lims=[6.9,8.38] + #logN2Ha=np.log10(self.N26584/self.Ha) CHECK!! why remove dust correction?? + #Z_new_N2Ha_lims= np.atleast_2d([1.0,1.0]).T*nppoly.polyval(self.logN2Ha,[7.04, 5.28,6.28,2.37])- + #np.atleast_2d( logq_lims).T*nppoly.polyval(self.logN2Ha,[-2.44,-2.01,-0.325,0.128])+ + #np.atleast_2d(logq_lims).T*(10**(self.logN2Ha-0.2)*(-3.16+4.65*self.logN2Ha)) + # R23 diagnostics from Kobulnicky & Kewley 2004 + + Zmax = np.zeros(self.nm) + # ionization parameter form logR23 + if not self.hasO3O2: + logq = np.zeros(self.nm) + else: + if self.Z_init_guess is None: + self.initialguess() + Z_new = self.Z_init_guess.copy() + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + else: + logqold, convergence, ii = np.zeros(self.nm) + 100, 100, 0 + tol = 1e-4 + #3 iterations are typically enought to achieve convergence KE08 A2.3 + while convergence > tol and ii < 100: + Zmax = Zmax * 0.0 + ii += 1 + logq = self.calclogq(Z_new) + Zmax[(logq >= 6.7) * (logq < 8.3)] = 8.4 + # maximum of R23 curve: + Z_new = nppoly.polyval(self.logR23, [9.72, -0.777, -0.951, -0.072, -0.811]) - \ + logq * nppoly.polyval(self.logR23, [0.0737, -0.0713, -0.141, 0.0373, -0.058]) + indx = self.Z_init_guess <= Zmax + Z_new[indx] = nppoly.polyval(self.logR23[indx], [9.40, 4.65, -3.17]) - \ + logq[indx] * nppoly.polyval(self.logR23[indx], [0.272, 0.547, -0.513]) + convergence = np.abs((logqold - logq).mean()) + logqold = logq.copy() + if ii >= 100: + printsafemulti("WARNING [KK04_R23]: loop did not converge", self.logf, self.nps) + Z_new = np.zeros(self.nm) + float('NaN') + Z_new_lims = [nppoly.polyval(self.logR23, [9.40, 4.65, -3.17]) - \ + logq * nppoly.polyval(self.logR23, [0.272, 0.547, -0.513]), + nppoly.polyval(self.logR23, [9.72, -0.777, -0.951, -0.072, -0.811]) - \ + logq * nppoly.polyval(self.logR23, [0.0737, -0.0713, -0.141, 0.0373, -0.058])] + Z_new[(Z_new_lims[0] > Z_new_lims[1])] = None + self.mds['KK04_R23'] = Z_new + + #@profile + def calcKDcombined(self): + # KD02comb Kewley, L. J., & Dopita, M. A., 2002, ApJ + # updated in KE08 + # ### KD02 [NII]/[OII] estimate ### + # (can be used for log(O/H)+12 > 8.6 only) + + printsafemulti("calculating KD_combined", self.logf, self.nps) + + #We first use the + #[N ii]/[O ii] ratio to determine whether it lies on the upper + #or lower R23 branch + + if self.mds['KD02_N2O2'] is None: + self.calcKD02_N2O2() + if self.mds['KK04_N2Ha'] is None: + self.calcKK04_N2Ha() + if self.logR23 is None: + self.calcR23() + if self.mds['M91'] is None: + printsafemulti("WARNING: Must first calculate M91", self.logf, self.nps) + self.calcM91() +# if self.mds['Z94'] is None: +# printsafemulti( "WARNING: Must first calculate Z94",self.logf,self.nps) +# self.calcZ94() + if self.mds['KK04_R23'] is None: + printsafemulti("WARNING: Must first calculate KK04_R23", self.logf, self.nps) + self.calcKK04_R23() + if not self.hasHa and not self.hasHb: + printsafemulti("WARNING: need Ha and Hb for this. did you run setHab()?", self.logf, self.nps) + + #alternative way to calculate KD02_N2O2, stated in the paper KD02, + #valid in high Z regimes (Z>8.4) + #but we forego it + #if not self.logN2O2 is None: + # self.mds['KD02_N2O2']=np.log10(8.511e-4*(1.54020+1.26602*self.logN2O2+0.167977*self.logN2O2**2))+12. + #else: self.mds['KD02_N2O2']=np.zeros(self.nm)+float('NaN') + + # ionization parameter + # calculate an initial ionization parameter by assuming + # a nominal lower branch [12 + log (O/H ) = 8.2] + # or upper branch [12 + log (O/H ) = 8.7] metallicity using + # equation (13) from KK04 + logq = np.zeros(self.nm) + if self.hasN2 and self.hasO2 and self.hasHb and self.hasHa and self.hasO3O2: + logq = self.calclogq(self.mds['KD02_N2O2']) + logq[self.mds['KD02_N2O2'] >= 8.4] = self.logq[self.mds['KD02_N2O2'] >= 8.4] + else: + if self.Z_init_guess is None: + self.initialguess() + logq = self.calclogq(self.Z_init_guess) + #FED: CHECK: the paragraph below makes sense in words but i dont see where it is enforced. + # if log([NII]/[OII]) after extinction correction is <-1.5, then check the data. + # if it is only slightly less than 1.5, then this can be a result of either noisy + # data, inaccurate fluxes or extinction correction, or a higher ionization parameter + # than modelled. + # For these cases, the average of the M91,Z94 and C01 should be used. + + # KD02 R23 estimate (not reliable for 8.4 < log(O/H)+12 < 8.8) + # uses [NII]/[OII] estimate as initial guess - this can be changed below + + self.mds['KD02comb'] = np.zeros(self.nm) + float('NaN') + + indx_ig = self.Z_init_guess > 8.4 + if self.mds['KD02_N2O2'] is not None: + self.mds['KD02comb'][indx_ig] = self.mds['KD02_N2O2'][indx_ig].copy() + if self.mds['KK04_N2Ha'] is not None: + self.mds['KD02comb'][~indx_ig] = self.mds['KK04_N2Ha'][~indx_ig].copy() + if self.mds['KK04_R23'] is not None and self.mds['M91'] is not None: + # if [NII]/[OII] abundance available + # and [NII]/Ha abundance < 8.4, then use R23. + indx = (~np.isnan(self.mds['KK04_R23'])) * (~np.isnan(self.mds['M91'])) * (~indx_ig) + self.mds['KD02comb'][indx] = 0.5 * (self.mds['KK04_R23'][indx].copy() + self.mds['M91'][indx].copy()) + + else: + printsafemulti("WARNING: cannot calculate KK04comb because KK04_R23 or M91, failed", self.logf, self.nps) + +#######################these are the metallicity diagnostics################## +#@profile + def calcPM14(self): + # Perez-Montero 2014 + # (can be used for for log(O/H)+12 > 8.6 only) + import os + from subprocess import Popen, PIPE, STDOUT + from StringIO import StringIO + + printsafemulti("calculating HIICHI", self.logf, self.nps) + fin_hii_chi = open(os.getenv('HIICHI_DIR') + '/in.tmp', 'w') + + if not self.hasHb: + printsafemulti("cannot calculate HIICHI without Hbeta", self.logf, self.nps) + return -1 + + ratios = np.zeros((5, self.nm)) + + if self.R2 is not None: + ratios[0] = self.R2 + elif self.hasO2: + ratios[0] = ((self.O23727 / self.Hb) + * self.dustcorrect(k_O2, k_Hb, flux=True)) + else: + ratios[0] = np.array(['0 '] * self.nm) + + #we will never have 4363... + ratios[1] = np.zeros(self.nm) + + if self.hasO3Hb: + ratios[2] = self.O3Hb + elif self.hasO3: + ratios[2] = ((self.O35007 / self.Hb) + * self.dustcorrect(k_O35007, k_Hb, flux=True)) # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + else: + ratios[2] = np.zeros(self.nm) + + if self.hasN2: + ratios[3] = self.N26584 / self.Hb + else: + ratios[3] = np.zeros(self.nm) + + if self.hasS2Hb: + ratios[4] = self.S2Hb + elif self.hasS2 and self.hasS26731: + ratios[4] = (((self.S26717 + self.S26731) / self.Hb) * + self.dustcorrect(k_S2, k_Hb, flux=True)) + else: + ratios[4] = np.zeros(self.nm) + + + for ni in range(self.nm): + fin_hii_chi.write('%f %f %f %f %f\n' % (ratios[0][ni], ratios[1][ni], ratios[2][ni], ratios[3][ni], ratios[4][ni])) + fin_hii_chi.close() + os.system("ln -s %s/C13*dat . " % os.getenv('HIICHI_DIR')) + print ("\n\n\n\n\n") + #os.system("python %s/HII-CHI-mistry_v01.2.py in.tmp"%os.getenv('HIICHI_DIR')) + #os.system("python %s/HII-CHI-mistry_v01.2.py %s/in.tmp"%(os.getenv('HIICHI_DIR'),os.getenv('HIICHI_DIR'))) + p = Popen(['python', '%s/HII-CHI-mistry_v01.2.py' % os.getenv('HIICHI_DIR'), + '%s/in.tmp' % os.getenv('HIICHI_DIR')], + stdout=PIPE, stdin=PIPE, stderr=STDOUT) + out, err = p.communicate(input='%s/in.tmp' % os.getenv('HIICHI_DIR')) + print ("\n\n\n\n\n") + out = StringIO(out) + # for l in enumerate(out): + # if l[0].isdigit(): + # break + out = out.readlines()[12:] + self.mds['PM14'] = np.zeros((self.nm)) + self.mds['PM14err'] = np.zeros((self.nm)) + for i, l in enumerate(out): + self.mds['PM14'][i], self.mds['PM14err'][i] = map(float, l.split()[3:5]) + #data = np.loadtxt(out, skiprows=12, usecols=(3,4))#, dtype=[('lOH','f'),('elOH','f')], delimiter=",", unpack = True) + print (self.mds) + os.system('rm -r C13*dat') + + +########## jch additions + + def calcPMC09(self): + #Perez-Montero & Contini (2009) - N2Ha + printsafemulti("calculating PMC09", self.logf, self.nps) + + if not self.hasHa or not self.hasN2 or not self.hasHa: + printsafemulti("WARNING: need N2, Ha ", + self.logf, self.nps) + return -1 + PMC09_N2 = lambda x: 9.07+0.79*x + + y = 9.07 + 0.78*self.logN2Ha + self.mds["PMC09_N2Ha"] = y + + #@profile + def calcB07(self): + #Bresolin 2007 - N2O2 + printsafemulti("calculating B07", self.logf, self.nps) + + if not self.hasHa or not self.hasN2 or not self.hasO2: + printsafemulti("WARNING: need N2, O2 ", + self.logf, self.nps) + return -1 + + # Use [N2/Ha * Ha/Hb] / [O2/Hb] + x = (self.logN2Ha+np.log10(2.86)) - self.logO2Hb + + y = 8.66 + 0.36*self.logN2O2 - 0.17*self.logN2O2**2 + self.mds["B07_N2O2"] = y + index = (self.logN2O2 < -1.2) + self.mds["B07_N2O2"][index] = float('NaN') + index = (self.logN2O2 > 0.6) + self.mds["B07_N2O2"][index] = float('NaN') + + + #@profile + def calcD16(self): + #Dopita+ 2016 + printsafemulti("calculating D16", self.logf, self.nps) + + if not self.hasHa or not self.hasN2 or not self.hasS2: + printsafemulti("WARNING: need N2, Ha and SII, ", + self.logf, self.nps) + return -1 + + d16_n2s2 = np.log10(self.N26584 / (self.S26717+self.S26731)) + self.dustcorrect(k_N2, k_S2, flux=False) + y = d16_n2s2 + 0.264 * self.logN2Ha + self.mds["D16"] = 8.77 + y# + 0.45 * pow(y + 0.3, 5) + index = (y < -1.) + self.mds["D16"][index] = float('NaN') + index = (y > 0.5) + self.mds["D16"][index] = float('NaN') + + + def calcC17(self, allC17=False): + # Curti+ 2017 + # Monthly Notices Royal Astronomical Society, vol. 465, pp 1384-1400 + # Published in October 2016 + # + # Added 5/16/17 by jch, Santiago + import pdb + + valid_upper = 8.85 + valid_lower = 7.6 + + printsafemulti("calculating C17", self.logf, self.nps) + highZ = None + + if self.logO35007O2 is not None: + # C17 O3O2 + self.mds['C17_O3O2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O3O2']] * self.nm).T + coefs[0] = coefs[0] - self.logO35007O2 + + # sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + + # Find valid solutions + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + + # the two cumsum assure that if the condition for the ith element + # of indx is [False, False] then after the first cumsum(1) is [0,0] + # [False, True] is [0,1] + # [True, True] is [1,2] + # but (here is the kicker) [True, False] is [1,1]. + # Because i want only one solution + # (i'll settle for the first one occurring) [1,1] is ambiguous. + # The second cumsum(1) makes + # [0,0]->[0,0], [0,1]->[0,1], [1,2]->[1,3] and finally [1,1]->[1,2] + + # Set the results where appropriate + self.mds['C17_O3O2'][(indx.sum(1)) > 0] = sols[indx].real + + # Now use this to see if we're on the high-Z branch of R23 + highZ = np.median(self.logO35007O2) < 0 + + if self.logN2Ha is not None: + # C17 N2Ha + self.mds['C17_N2Ha'] = np.zeros(self.nm) + float('NaN') + + coefs = np.array([C17_coefs['N2Ha']] * self.nm).T + coefs[0] = coefs[0] - self.logN2Ha + + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + + self.mds['C17_N2Ha'][(indx.sum(1)) > 0] = sols[indx].real + if highZ is None: + highZ = np.median(self.logN2Ha) > -1.3 + + if self.hasO3 and self.hasN2: + # C17 O3N2 + self.mds['C17_O3N2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O3N2']] * self.nm).T + o3n2_value = self.logO3Hb - self.logN2Ha + coefs[0] = coefs[0] - o3n2_value + + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) \ + * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O3N2'][(indx.sum(1)) > 0] = sols[indx].real + + if self.logO3Hb is not None: + # C17 O3Hb + self.mds['C17_O3Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O3Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO3Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + + # C17 define this only for the high-metal branch. + indx = ((sols.real >= 8.2) * (sols.real <= valid_upper) * + (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O3Hb'][(indx.sum(1)) > 0] = sols[indx].real + + # Require allC17 flag if we want everything. + if not allC17: + return + else: + printsafemulti("calculating other C17s", self.logf, self.nps) + + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute C17_R23 without R23", self.logf, self.nps) + else: + self.mds['C17_R23'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['R23']] * self.nm).T + coefs[0] = coefs[0] - self.logR23 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0) * + (sols.real >= 8.4)).cumsum(1).cumsum(1) == 1 + self.mds['C17_R23'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0) * + (sols.real < 8.4)).cumsum(1).cumsum(1) == 1 + self.mds['C17_R23'][(indx.sum(1)) > 0] = sols[indx].real + + + if self.logO2Hb is not None: + self.mds['C17_O2Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O2Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO2Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= valid_lower) * (sols.real <= 8.3) * + (sols.imag == 0) * + (sols.real >= 8.7)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= valid_lower) * (sols.real <= 8.3) * + (sols.imag == 0) * + (sols.real <= valid_upper)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + + + + def calcPG16(self): + # Pilyugin & Grebel (2016, MNRAS, 457, 3678) + printsafemulti("calculating PG16", self.logf, self.nps) + + # Initialize the PG16 results + self.mds['PG16'] = np.zeros(self.nm) + float('NaN') + + # Make sure the lines are all here... + if not self.hasHa or not self.hasN2 or not self.hasO2 or not self.hasO3 or not self.hasS2: + printsafemulti("WARNING: need O2, O3, N2, S2, and Hb", + self.logf, self.nps) + return -1 + + pilR2 = self.logO2Hb + pilR3 = self.logO3Hb+np.log10(1.33) # Both members of doublet + pilR3R2 = (pilR3-pilR2) + + # Calculate S2/Hb = S2/Ha * Ha/Hb = S2/Ha*2.85 + pilS2 = np.log10((self.S26731 + self.S26731) / self.Ha * 2.85) + pilS2 += np.log10(self.dustcorrect(k_S2,k_Ha,flux=True)) + pilR3S2 = (pilR3-pilS2) + + # Calculate N2/Hb = N2/Ha * Ha/Hb = N2/Ha*2.85 + pilN2 = np.log10((self.N26584*1.33 / self.Ha)*2.85) + # pilN2 += np.log10(self.dustcorrect(k_N2,k_Hb,flux=True)) + + # Determine which branch we're on + highZ = (np.median(pilN2) >= -0.6) + print(np.median(pilN2)) + + # Calculate R = O2O3N2 abundances + if highZ == True: + print('PG16 high metal branch') + self.mds['PG16_R'] = 8.589+0.022*pilR3R2+0.399*pilN2 + self.mds['PG16_R'] += (-0.137+0.164*pilR3R2+0.589*pilN2)*pilR2 + else: + print('PG16 low metal branch') + self.mds['PG16_R'] = 7.932+0.944*pilR3R2+0.695*pilN2 + self.mds['PG16_R'] += (0.970-0.291*pilR3R2-0.019*pilN2)*pilR2 + + # Calculate S = S2O3N2 abundances + if highZ == True: + print('PG16 high metal branch') + self.mds['PG16_S'] = 8.424+0.030*pilR3S2+0.751*pilN2 + self.mds['PG16_S'] += (-0.349+0.182*pilR3S2+0.508*pilN2)*pilS2 + else: + print('PG16 low metal branch') + self.mds['PG16_S'] = 8.072+0.789*pilR3S2+0.726*pilN2 + self.mds['PG16_S'] += (1.069-0.17*pilR3S2+0.022*pilN2)*pilS2 diff --git a/build/lib/pyMCZ/pylabsetup.py b/build/lib/pyMCZ/pylabsetup.py new file mode 100644 index 0000000..5ffef05 --- /dev/null +++ b/build/lib/pyMCZ/pylabsetup.py @@ -0,0 +1,41 @@ +import matplotlib as mpl +import pylab as plt + +mpl.rcParams.update(mpl.rcParamsDefault) +mpl.rcParams['font.size'] = 26. +mpl.rcParams['font.family'] = 'serif' +#mpl.rcParams['font.family'] = 'serif' +mpl.rcParams['font.serif'] = ['Times New Roman', 'Times', 'Palatino', 'Charter', 'serif'] +mpl.rcParams['font.sans-serif'] = ['Helvetica'] +mpl.rcParams['axes.labelsize'] = 24 +mpl.rcParams['xtick.labelsize'] = 22. +mpl.rcParams['ytick.labelsize'] = 22. +mpl.rcParams['xtick.major.size'] = 15. +mpl.rcParams['xtick.minor.size'] = 10. +mpl.rcParams['ytick.major.size'] = 15. +mpl.rcParams['ytick.minor.size'] = 10. +#mpl.rcParams['figure.autolayout']= True + +#fontsize=26 +#mpl.rc('axes', titlesize=fontsize) +#mpl.rc('axes', labelsize=fontsize) +#mpl.rc('xtick', labelsize=fontsize) +#mpl.rc('ytick', labelsize=fontsize) +#mpl.rc('font', size=fontsize, family='serif', serif='Utopia', +# style='normal', variant='normal', +# stretch='normal', weight='normal') +#mpl.rc('font',**{'family':'serif','serif':[ 'Times New Roman', 'Times', 'serif'], +# 'sans-serif':['Helvetica'], 'size':19, +# 'weight':'normal'}) +mpl.rc('axes', **{'linewidth' : 1.2}) +mpl.rc('axes', **{'labelweight': 'normal', 'linewidth': 1}) +mpl.rc('axes', **{'labelweight': 'normal', 'linewidth': 1}) +mpl.rc('ytick', **{'major.pad': 5, 'color': 'k'}) +mpl.rc('xtick', **{'major.pad': 5, 'color': 'k'}) +params = {'legend.fontsize': 24, + 'legend.numpoints': 1, + 'legend.handletextpad': 1 + } + +plt.rcParams.update(params) +plt.minorticks_on() diff --git a/build/lib/pyMCZ/testcompleteness.py b/build/lib/pyMCZ/testcompleteness.py new file mode 100644 index 0000000..43a317b --- /dev/null +++ b/build/lib/pyMCZ/testcompleteness.py @@ -0,0 +1,136 @@ +import os +import sys +import numpy as np +import pylab as pl + +from scipy import stats # , interpolate +import pylabsetup + +#pickle may not be installed +NOPICKLE = False +try: + import pickle +except ImportError: + NOPICKLE = True + + +def fitdistrib(picklefile, scales=None): + from matplotlib.font_manager import findfont, FontProperties + from matplotlib.ticker import FormatStrFormatter + majorFormatter = FormatStrFormatter('%.2f') + + fs = 20 # FontProperties.get_size(FontProperties()) + if 'Time' not in findfont(FontProperties()): + fs = 20 + print "FONT: %s, %d" % (findfont(FontProperties()), fs) + fs = fs - 2 + params = {'axes.labelsize': fs, + 'xtick.labelsize': fs - 3, + 'ytick.labelsize': fs - 3, + 'legend.fontsize': fs - 2, + 'legend.handletextpad': 0.2 + } + pl.rcParams.update(params) + + + if NOPICKLE: + print "you must install pickle to read in the distributions and fit them" + return -1 + assert(os.path.isfile(picklefile)), "missing pickled file %s" % picklefile + pklfile = open(picklefile, 'rb') + res = pickle.load(pklfile) + print scales + if scales: + testingdiags = scales.split(',') + else: + testingdiags = ['E(B-V)', 'D02', 'M13_N2', 'KD02comb'] + + print "\n\n###testing with scales: ", testingdiags, + print "###\n\n" + ebvs = res['E(B-V)'].T + nm = len(ebvs) + Ndata = len(ebvs[0]) + assert(Ndata > 0), "something is wrong with your distribution" + invalids = [sum(np.isnan(res[testingdiags[1]].T[i])) + sum(np.isnan(res[testingdiags[2]].T[i])) + sum(np.isnan(res[testingdiags[3]].T[i])) for i in range(nm)] + myi, = np.where(invalids == min(invalids)) + try: + myi = myi[1] # in case there are more than one solutions + except IndexError: + try: + myi = myi[0] + except IndexError: + pass + xold = np.zeros(10) + assert(Ndata - invalids[myi] > 0), "something is wrong with your distribution. Perhaps one of the default scale has all invalid values? try select different scales" + # -- plotting utils + fwid = 10. # fig width + rat = 1. # fig aspect ratio + offx = 0.08 # x offset in figure coords + offy = 0.05 # y offset in figure coords + psz = 0.4 # size of the plot in figure width coords + nr = 2 # number of row plots + nc = 2 # number of col plots + + fig, axx = pl.subplots(nr, nc, figsize=(fwid, fwid / rat)) + print fig + + for i, d in enumerate(testingdiags): + ii, jj = int(i / 2), int((i + 1) / 2) - int(i / 2) + ax = axx[ii][jj] + ax.set_position([jj / float(nc) + offx, 1.0 - (ii + 1) / float(nr) + offy, psz, psz * rat]) + for f in [0.1, 0.25, 0.5, 0.75, 1]: + n0 = int(f * Ndata) + x = np.random.choice(res[d].T[myi], n0, replace=False) + #print res[d].T[myi] + x = x[x > 0] + + x.sort() + #print x + n = len(x) + + Px_cuml = np.linspace(0, 1, n) + # set up an interpolation of the inverse cumulative distribution + #tck = interpolate.splrep(Px_cuml, x) + + # sample evenly along the cumulative distribution, and interpolate + #Px_cuml_sample = np.linspace(0, 1, 10 * n) + + #x_sample = interpolate.splev(Px_cuml_sample, tck) + + indices = np.linspace(0, n - 1, 20).astype(int) + ax.set_ylim(0, 1.05) + ax.plot(x[indices], Px_cuml[indices], 'o', lw=0, label="%d" % n0) + ax.plot(x, Px_cuml, '-k') + maxleft, maxright = min(x), max(x) + D, p = stats.ks_2samp(x, xold) + print "KS test for %s n = %d D = %.2g; p = %.2g" % (d, n0, D, p) + xold = x.copy() + lims = ax.set_xlim((maxleft, maxright)) + axratio = (lims[1] - lims[0]) / 1.05 + ax.set_aspect(aspect=axratio) + + ax.set_title('%s Cumulative Distribution' % d.replace('_', ' '), fontsize=fs) + ax.set_xlabel('$x$') + ax.set_ylabel('$p( maxleft)] + while (maxright - xticks[-1]) < 0.25 * dx: + xticks = xticks[:-1] + pl.xticks(xticks, ['%.2f' % s for s in xticks]) + pl.savefig(picklefile.replace('.pkl', '_testcomplete.pdf')) + pl.show() + +if __name__ == "__main__": + if len(sys.argv) > 2: + print '''only argument allowed: name of the pickle file containing the distribution + default is '../output/exampledata/exampledata_n2000.pkl' + ''' + sys.exit() + infile = '../output/exampledata/exampledata_n2000.pkl' + if len(sys.argv) > 1: + infile = sys.argv[1] + fitdistrib(infile) diff --git a/build/scripts-2.7/mcz.py b/build/scripts-2.7/mcz.py new file mode 100755 index 0000000..11992a1 --- /dev/null +++ b/build/scripts-2.7/mcz.py @@ -0,0 +1,747 @@ +#!/Users/howk/anaconda/bin/python +import os +import sys +import argparse +import warnings + +import numpy as np +import scipy.stats as stats +from scipy.special import gammaln +from scipy import optimize +import matplotlib.pyplot as plt +from matplotlib.ticker import FormatStrFormatter +import csv as csv + +#modules of this package +import pylabsetup + +#import metallicity_save2 as metallicity +import metallicity as metallicity + +import itertools +import multiprocessing as mpc + +###versin 1.3 February 2016 +#fixed compatyibiity issue with numphy >1.9 in the val to np.histogram https://github.com/numpy/numpy/issues/6469 + +###version 1.3.1 April 2016 +#works with version 0.7 of pyqz (current version) +#previous versions only worked with version 0.5. +#a check for the version of pyqz is implememted and a different call is issued for version >0.5 and <=0.5 + + + +# Define the version of +__version__ = '1.3.1' + +NM0 = 0 # setting this to say N>0 starts the calculation at measurement N. +#this is only for exploratory purposes as the code bugs out before +#plotting and printing the results + +PROFILING = True +PROFILING = False + +alllines = ['[OII]3727', 'Hb', '[OIII]4959', '[OIII]5007', '[OI]6300', 'Ha', '[NII]6584', '[SII]6717', '[SII]6731', '[SIII]9069', '[SIII]9532'] +morelines = ['E(B-V)', 'dE(B-V)', 'scale_blue', 'd scale_blue'] + +MAXPROCESSES = 10 + +#pickle may not be installed +NOPICKLE = False +try: + import pickle +except ImportError: + NOPICKLE = True + +CLOBBER = False +VERBOSE = False +UNPICKLE = False +ASCIIOUTPUT = False +ASCIIDISTRIB = False +RUNSIM = True +NOPLOT = False +BINMODE = 'k' +binning = {'bb': 'Bayesian blocks', 'k': "Knuth's rule", 'd': "Doane's formula", 's': r'$\sqrt{N}$', 't': r'$2 N^{1/3}$', 'kd': 'Kernel Density'} + +MP = False + + +def is_number(s): + if not type(s) is np.string_: + try: + float(s) + return True + except ValueError: + return False + return False + + +def smart_open(logfile=None): + if logfile and logfile != '-': + fh = open(logfile, 'w') + else: + fh = sys.stdout + return fh + + +def getknuth(m, data, N): + m = int(m) + if m > N: + return [-1] + bins = np.linspace(min(data), max(data), int(m) + 1) + try: + nk, bins = np.histogram(data, bins) + return -(N * np.log(m) + gammaln(0.5 * m) - m * gammaln(0.5) - gammaln(N + 0.5 * m) + np.sum(gammaln(nk + 0.5))) + except: + return [-1] + + +def knuthn(data, maxM=None): + assert data.ndim == 1, "data must be 1D array to calculate Knuth's number of bins" + N = data.size + if not maxM: + maxM = 5 * np.sqrt(N) + m0 = 2.0 * N ** (1. / 3.) + gk = getknuth + if gk == [-1]: + return m0, 't' + mkall = optimize.fmin(gk, m0, args=(data, N), disp=VERBOSE, maxiter=30) # , maxfun=1000)#[0] + mk = mkall[0] + if mk > maxM or mk < 0.3 * np.sqrt(N): + mk = m0 + return mk, 't' + return mk, 0 + + +############################################################################## +##The input data +############################################################################## +##Reads the flux file and returns it as an array. +##Ignores non-numeric lines +##Returns (flux array,num) +############################################################################## +def readfile(filename): + noheader = 1 + findex = -1 + f = open(filename, 'r') + l0 = f.readline().replace(' ', '') + l1 = f.readline().split() + if l0.startswith('#') or l0.startswith(';'): + header = l0.strip().replace(";", '').replace("#", '').split(',') + header[0] = header[0].replace(' ', '') + header = header[:len(l1)] + else: + noheader = 0 + header = ['galnum'] + alllines + ['flag'] + morelines + header = header[:len(l1)] + + formats = ['S10'] + ['f'] * (len(header) - 1) + if 'flag' in header: + findex = header.index('flag') + formats[findex] = 'S10' + + bstruct = {} + for i, k in enumerate(header): + bstruct[k] = [i, 0] + b = np.loadtxt(filename, skiprows=noheader, dtype={'names': header, 'formats': formats}, comments=';') + if b.size == 1: + b = np.atleast_1d(b) + + for i, k in enumerate(header): + if not k == 'flag' and is_number(b[k][0]): + bstruct[k][1] = np.count_nonzero(b[k]) + sum(np.isnan(b[k])) + j = len(b['galnum']) + return b, j, bstruct + + +def ingest_data(filename, path): + ###Initialize### + measfile = os.path.join(path, filename + "_meas.txt") + errfile = os.path.join(path, filename + "_err.txt") + + ###read the max, meas, min flux files### + meas, nm, bsmeas = readfile(measfile) + err, nm, bserr = readfile(errfile) + try: + snr = (meas[: + ,1:].view(np.float32).reshape(meas[: + ,1:].shape + (-1, ))[: + ,1:]) / (err[: + ,1:].view(np.float32).reshape(err[: + ,1:].shape + (-1, ))[: + ,1:]) + if snr[~np.isnan(snr)].any() < 3: + raw_input('''WARNING: signal to noise ratio smaller than 3 + for at least some lines! You should only use SNR>3 + measurements (return to proceed)''') + except (IndexError, TypeError): + pass + return (filename, meas, err, nm, path, (bsmeas, bserr)) + + +def input_data(filename, path): + p = os.path.join(path, "input") + assert os.path.isdir(p), "bad data directory %s" % p + if os.path.isfile(os.path.join(p, filename + '_err.txt')): + if os.path.isfile(os.path.join(p, filename + '_meas.txt')): + return ingest_data(filename, path=p) + print "Unable to find _meas and _err files ", filename + '_meas.txt', filename + '_err.txt', "in directory ", p + return -1 + + +############################################################################## +##returns a random distribution. In the deployed version of the code this is a gaussian distribution, but the user can include her or his distribution. +##return a random sample of size n +############################################################################## +def errordistrib(distrargs, n, distype='normal'): + if distype == 'normal': + try: + mu, sigma = distrargs + except: + print "for normal distribution distargs must be a 2 element tuple" + return -1 + return np.random.normal(mu, sigma, n) + else: + print "distribution not supported" + return -1 + + +############################################################################## +##returns appropriate bin size for the number of data +##mode 'k' calculates this based on Knuth's rule +##mode 'd' calculates this based on Doane's formula +##mode 's' calculates this based on sqrt of number of data +##mode 't' calculates this based on 2*n**1/3 (default) +############################################################################## +def getbinsize(n, data, ): + if BINMODE == 'd': + g1 = np.abs(stats.mstats.moment(data, moment=3)) # /data.std()) + s1 = np.sqrt(float(n) / 6.0) + #s1=1.0/np.sqrt(6.*(n-2.)/((n+1.)*(n+3.))) + k = 1 + np.log2(n) + np.log2(1 + (g1 * s1)), 0 + elif BINMODE == 's': + k = np.sqrt(n), 0 + elif BINMODE == 't': + k = 2. * n ** (1. / 3.), 0 + else: + k = knuthn(data) + return k + + +############################################################################## +##Check if hist files already exist and need to be replaced +############################################################################## +def checkhist(snname, Zs, nsample, i, path): + global CLOBBER + + name = '%s_n%d_%s_%d' % ((snname, nsample, Zs, i + 1)) + outdir = os.path.join(path, 'hist') + outfile = os.path.join(outdir, name + ".pdf") + if os.path.isfile(outfile) and not CLOBBER: + replace = raw_input("replacing existing image files, starting with: %s ? [Y/n]\n" % outfile).lower() + assert(not (replace.startswith('n'))), "save your existing output directory under another name first" + CLOBBER = True + + +############################################################################## +##Save the result as histogram as name +############################################################################## +#@profile +def savehist(data, snname, Zs, nsample, i, path, nmeas, measnames, verbose=False, fs=24, reserr=None): + global BINMODE + #global NOPLOT + + name = '%s_n%d_%s_%d' % ((snname, nsample, Zs, i + 1)) + outdir = os.path.join(path, 'hist') + outfile = os.path.join(outdir, name + ".pdf") + if not NOPLOT: + fig = plt.figure(figsize=(11, 8)) + plt.clf() + + ####kill outliers, infinities, and bad distributions### + data = data[np.isfinite(data)] + + n = data.shape[0] + kde = None + if not n > 0: + if verbose: + print "data must be an actual distribution (n>0 elements!, %s)" % Zs + return "-1,-1,_1", [], kde + + if data.shape[0] <= 0 or np.sum(data) <= 0: + print '{0:15} {1:20} {2:>13d} {3:>7d} {4:>7d} '.format(snname, Zs, -1, -1, -1) + return "-1, -1, -1", [], kde + try: + ###find C.I.### + median, pc16, pc84 = np.percentile(data, [50, 16, 84]) + std = np.std(data) + left = pc16 + right = pc84 + maxleft = median - std * 5 + maxright = median + std * 5 + if "%2f" % maxright == "%2f" % maxleft: + maxleft = median - 1 + maxright = median + 1 + if round(right, 6) == round(left, 6) and round(left, 6) == round(median, 6): + print '{0:15} {1:20} {2:>13.3f} -{3:>7.3f} +{4:>7.3f} (no distribution)'.format(snname, Zs, median, 0, 0) + if reserr: + print '+/- {0:.3f}'.format(reserr) + return "%f\t %f\t %f" % (round(median, 3), round(median - left, 3), round(right - median, 3)), data, kde # "-1,-1,-1",[] + ###print out the confidence interval### + print '{0:15} {1:20} {2:>13.3f} -{3:>7.3f} +{4:>7.3f}'.format(snname, Zs, round(median, 3), round(median - left, 3), round(right - median, 3)) + if reserr: + print '+/- {0:.3f}'.format(reserr) + alpha = 1.0 + + ######histogram###### + if BINMODE == 'kd': + ##if sklearn is available use it to get Kernel Density + try: + from sklearn.neighbors import KernelDensity + except ImportError: + print '''sklearn is not available, + thus we cannot compute kernel density. + switching to bayesian blocks''' + BINMODE = 'bb' + if BINMODE == 'kd': + ##bw is chosen according to Silverman 1986 + bw = 1.06 * std * n ** (-0.2) + numbin, bm = getbinsize(data.shape[0], data) + distrib = np.histogram(data, bins=int(numbin), density=True) + ###make hist### + counts, bins = distrib[0], distrib[1] + widths = np.diff(bins) + countsnorm = counts / np.max(counts) + + if bw > 0: + kde = KernelDensity(kernel='gaussian', bandwidth=bw).fit(data[: + , np.newaxis]) + kdebins = np.linspace(maxleft, maxright, 1000)[: + , np.newaxis] + log_dens = kde.score_samples(kdebins) + dens = np.exp(log_dens) + norm = countsnorm.sum() * (bins[1] - bins[0]) / dens.sum() / (kdebins[1] - kdebins[0]) + if not NOPLOT: + plt.fill(kdebins[: + ,0], dens * norm, fc='#7570b3', alpha=0.8) + alpha = 0.5 + + ###find appropriate bin size### + else: + if BINMODE == 'bb': + ##if astroML is available use it to get Bayesian blocks + bm = 0 + try: + from astroML.plotting import hist as amlhist + if BINMODE == 'bb': + distrib = amlhist(data, bins='blocks', normed=True) + if not NOPLOT: + plt.clf() + except ImportError: + print "bayesian blocks for histogram requires astroML to be installed" + print "defaulting to Knuth's rule " + ##otherwise + numbin, bm = getbinsize(data.shape[0], data) + distrib = np.histogram(data, bins=int(numbin), density=True) + else: + numbin, bm = getbinsize(data.shape[0], data) + distrib = np.histogram(data, bins=int(numbin), density=True) + + ###make hist### + counts, bins = distrib[0], distrib[1] + widths = np.diff(bins) + countsnorm = counts / np.max(counts) + + ###plot hist### + if NOPLOT: + return "%f\t %f\t %f" % (round(median, 3), round(median - left, 3), round(right - median, 3)), data, kde + + plt.bar(bins[:-1], countsnorm, widths, color=['gray'], alpha=alpha) + plt.minorticks_on() + plt.gca().xaxis.set_major_formatter(FormatStrFormatter('%.2f')) + plt.xlim(maxleft, maxright) + + #the following lines assure the x tick label is + #within the length of the x axis + xticks = plt.xticks()[0] + dx = xticks[-1] - xticks[-2] + xticks = xticks[(xticks < maxright) * (xticks > maxleft)] + if (maxright - xticks[-1]) < 0.25 * dx: + maxright = maxright + 0.25 * dx + maxleft = maxleft - 0.25 * dx + plt.xlim(maxleft, maxright) + plt.xticks(xticks, ['%.2f' % s for s in xticks]) + + plt.ylim(0, 1.15) + plt.yticks(np.arange(0.2, 1.3, 0.2), ["%.1f" % x for x in np.arange(0.2, 1.1, 0.2)]) + plt.axvspan(left, right, color='DarkOrange', alpha=0.4) + plt.axvline(x=median, linewidth=2, color='white', ls='--') + + #labels and legends + st = '%s ' % (snname) + plt.annotate(st, xy=(0.13, 0.6), xycoords='axes fraction', size=fs, fontweight='bold') + st = '%s ' % (Zs.replace('_', ' ')) + plt.annotate(st, xy=(0.61, 0.93), xycoords='axes fraction', fontsize=fs, fontweight='bold') + st = 'measurement %d of %d\n %s\nmedian: %.3f\n16th Percentile: %.3f\n84th Percentile: %.3f' % (i + 1, nmeas, measnames[i], round(median, 3), round(left, 3), round(right, 3)) + plt.annotate(st, xy=(0.61, 0.65), xycoords='axes fraction', fontsize=fs) + effectiven = len(data[~np.isnan(data)]) + if effectiven: + st = 'MC sample size %d (%d)\nhistogram rule: %s' % (effectiven, nsample, binning[BINMODE]) + if bm: + if effectiven < nsample: + st = 'MC sample size %d (%d)\nhistogram rule: %s' % (effectiven, nsample, binning[bm]) + else: + st = 'MC sample size %d\nhistogram rule: %s' % (nsample, binning[bm]) + plt.annotate(st, xy=(0.61, 0.55), xycoords='axes fraction', fontsize=fs - 5) + if "E(B-V)" in Zs: + plt.xlabel('E(B-V) [mag]') + outfile = outfile.replace('(', '').replace(')', '') + elif "logR23" in Zs: + plt.xlabel('logR23') + else: + plt.xlabel('12+log(O/H)') + plt.ylabel('relative counts') + plt.savefig(outfile, format='pdf') + plt.close(fig) + + return "%f\t %f\t %f" % (round(median, 3), round(median - left, 3), round(right - median, 3)), data, kde + + except (OverflowError, AttributeError, ValueError): + if VERBOSE: + print data + print name, 'had infinities (or something in plotting went wrong)' + return "-1, -1,-1", [], None + + +def calc((i, (sample, flux, err, nm, bss, mds, disp, dust_corr, verbose, res, scales, nps, logf))): + logf = sys.stdout + print >> logf, "\n\nreading in measurements ", i + 1 + fluxi = {} # np.zeros((len(bss[0]),nm),float) + for k in bss[0].iterkeys(): + print >> logf, '{0:15} '.format(k), + print >> logf, '{0:0.2} +/- {1:0.2}'.format(flux[k][i], err[k][i]) + fluxi[k] = flux[k][i] * np.ones(len(sample[i])) + err[k][i] * sample[i] + warnings.filterwarnings("ignore") + success = metallicity.calculation(scales[i], fluxi, nm, mds, nps, logf, disp=disp, dust_corr=dust_corr, verbose=verbose) + if success == -1: + print >> logf, "MINIMUM REQUIRED LINES: '[OII]3727','[OIII]5007','[NII]6584','[SII]6717'" + + for key in scales[i].mds.iterkeys(): + if key in res.keys(): + res[key][i] = scales[i].mds[key] + if res[key][i] is None: + res[key][i] = [float('NaN')] * len(sample) + return res + + +############################################################################## +## The main function. takes the flux and its error as input. +## filename - a string 'filename' common to the three flux files +## flux - np array of the fluxes +## err - the flux errors, must be the same dimension as flux +## nsample - the number of samples the code will generate. Default is 100 +## errmode - determines which method to choose the bin size. +## mode 'k' calculates this based on Knuth's rule (default) +## mode 'd' calculates this based on Doane's formula +## mode 's' calculates this based on sqrt of number of data +## mode 't' calculates this based on 2*n**1/3 +############################################################################## +#@profile +def run((name, flux, err, nm, path, bss), nsample, mds, multiproc, logf, unpickle=False, dust_corr=True, verbose=False, fs=24): + global RUNSIM # ,BINMODE#,NOPLOT + assert(len(flux[0]) == len(err[0])), "flux and err must be same dimensions" + assert(len(flux['galnum']) == nm), "flux and err must be of declaired size" + assert(len(err['galnum']) == nm), "flux and err must be same dimensions" + + #increasing sample by 10% to assure robustness against rejected samples + newnsample = nsample + if nsample > 1: + newnsample = int(nsample + 0.1 * nsample) + + p = os.path.join(path, '..') + + ###retrieve the metallicity keys + Zs = metallicity.get_keys() + Zserr = metallicity.get_errkeys() + + ###make necessary paths for output files + if not os.path.exists(os.path.join(p, 'output', '%s' % name)): + os.makedirs(os.path.join(p, 'output', '%s' % name)) + if not os.path.exists(os.path.join(p, 'output', '%s' % name, 'hist')): + os.makedirs(os.path.join(p, 'output', '%s' % name, 'hist')) + binp = os.path.join(p, 'output', '%s' % name) + picklefile = os.path.join(binp, '%s_n%d.pkl' % (name, nsample)) + if VERBOSE: + print "output files will be stored in ", binp + if not CLOBBER and not NOPLOT: + for key in Zs: + for i in range(NM0, nm): + checkhist(name, key, nsample, i, binp) + if unpickle: + RUNSIM = False + if not os.path.isfile(picklefile): + raw_input("missing pickled file for this simulation: name, nsample.\nrun the MonteCarlo? Ctr-C to exit, Return to continue?\n") + RUNSIM = True + else: + pklfile = open(picklefile, 'rb') + res = pickle.load(pklfile) + + if RUNSIM: + ###Sample 'nsample' points from a gaussian centered on 0 with std 1 + mu = 0 + sigma = 1 + dargs = (mu, sigma) + if nsample == 1: + sample = [np.array([mu]) for i in range(NM0, nm)] + else: + sample = [errordistrib(dargs, newnsample) for i in range(NM0, nm)] + if sample == -1: + return -1 + ###Start calculation### + ## the flux to be feed to the calculation will be + ## flux + error*i + ## where i is the sampled gaussian + if VERBOSE: + print "Starting iteration" + + #initialize the dictionary + res = {} + reserr = {} + for key in Zs: + res[key] = [[] for i in range(NM0, nm)] + for key in Zserr: + reserr[key] = [[] for i in range(NM0, nm)] + #use only valid inputs + delkeys = [] + for k in bss[0].iterkeys(): + if k == 'flag' or k == 'galnum' or bss[0][k][1] == float('nan'): # or bss[1][k][1]==bss[0][k][1]: + delkeys.append(k) + for k in delkeys: + del bss[0][k] + del bss[1][k] + + import metscales as ms + nps = min(mpc.cpu_count() - 1 or 1, MAXPROCESSES) + + if multiproc and nps > 1: + scales = [ms.diagnostics(newnsample, logf, nps) for i in range(nm)] + + print >> logf, "\n\n\nrunning on %d threads\n\n\n" % nps + second_args = [sample, flux, err, nm, bss, mds, VERBOSE, dust_corr, VERBOSE, res, scales, nps, logf] + pool = mpc.Pool(processes=nps) # depends on available cores + rr = pool.map(calc, itertools.izip(range(NM0, nm), itertools.repeat(second_args))) # for i in range(nm): result[i] = f(i, second_args) + for ri, r in enumerate(rr): + for kk in r.iterkeys(): + res[kk][ri] = r[kk][ri] + + for ri, r in enumerate(rr): + for kk in r.iterkeys(): + res[kk][ri] = r[kk][ri] + pool.close() # not optimal! but easy + pool.join() + for key in scales[i].mds.iterkeys(): + if key in Zs: + res[key] = np.array(res[key]).T + elif key in reserr.keys(): + reserr[key] = np.array(reserr[key]).T + if res[key][i] is None: + res[key][i] = [float('NaN')] * len(sample) + + if VERBOSE: + print "Iteration Complete" + else: + #looping over nm spectra + for i in range(NM0, nm): + scales = ms.diagnostics(newnsample, logf, nps) + print >> logf, "\n\n measurements ", i + 1 + fluxi = {} + + for k in bss[0].iterkeys(): + print >> logf, '{0:15} '.format(k), + print >> logf, '{0:0.2} +/- {1:0.2}'.format(flux[k][i], err[k][i]) + fluxi[k] = flux[k][i] * np.ones(len(sample[i])) + err[k][i] * sample[i] + warnings.filterwarnings("ignore") + print >> logf, "" + + success = metallicity.calculation(scales, fluxi, nm, mds, 1, logf, disp=VERBOSE, dust_corr=dust_corr, verbose=VERBOSE) + if success == -1: + print "MINIMUM REQUIRED LINES: [OII]3727 & [OIII]5007, or [NII]6584, and Ha & Hb if you want dereddening" + #continue + + + for key in scales.mds.iterkeys(): + if not key in Zs: + continue + res[key][i] = scales.mds[key] + if res[key][i] is None: + res[key][i] = [float('NaN')] * newnsample + elif len(res[key][i]) < newnsample: + res[key][i] = res[key][i] + [float('NaN')] * (newnsample - len(res[key][i])) + + for key in scales.mds.iterkeys(): + if key in Zs: + res[key] = np.array(res[key]).T + if VERBOSE: + print "Iteration Complete" + + #"WE CAN PICKLE THIS!" + #pickle this realization + if not NOPICKLE: + pickle.dump(res, open(picklefile, 'wb')) + + from matplotlib.font_manager import findfont, FontProperties + + if 'Time' not in findfont(FontProperties()): + fs = 20 + if VERBOSE: + print "FONT: %s, %d" % (findfont(FontProperties()), fs) + + ###Bin the results and save### + print "\n\n" + print '{0:15} {1:20} {2:>13} -{3:>5} +{4:>5} {5:11} {6:>7}'.format("SN", "diagnostic", "metallicity", "34%", "34%", "(sample size:", '%d)' % nsample) + for i in range(NM0, nm): + if ASCIIOUTPUT: + fi = open(os.path.join(binp, '%s_n%d_%d.txt' % (name, nsample, i + 1)), 'w') + fi.write("%s\t Median Oxygen abundance (12+log(O/H))\t 16th percentile\t 84th percentile\n" % name) + + boxlabels = [] + datas = [] + print "\n\nmeasurement %d : %s-------------------------------------------------------------" % (i + 1, flux[i]['galnum']) + for key in Zs: + if nsample == -1: + try: + if ~np.isnan(res[key][i][0]): + print '{0:15} {1:20} {2:>13.3f} -{3:>7.3f} +{4:>7.3f} (no distribution)'.format(name + ' %d' % (i + 1), key, res[key][i][0], 0, 0) + except IndexError: + pass + else: + reserr = None + try: + if sum(~np.isnan(res[key][: + ,i])) > 0: + if ASCIIDISTRIB: + with open(os.path.join(binp, '%s_n%d_%s_%d.csv' % (name, nsample, key, i + 1)), "wb") as fidist: + writer = csv.writer(fidist) + writer.writerow(res[key][: + ,i]) + if 'PM14' in key: + print reserr['PM14err'] + reserr = np.sqrt(~np.nansum(reserr['PM14err'][: + ,i] ** 2)) + sh, data, kde = savehist(res[key][: + ,i], name, key, nsample, i, binp, nm, flux[:]['galnum'], verbose=verbose, fs=fs, reserr=reserr) + s = key + "\t " + sh + '\n' + if ASCIIOUTPUT: + fi.write(s) + if key not in ["E(B-V)", "logR23"]: + boxlabels.append(key.replace('_', ' ')) + datas.append(data) + if BINMODE == 'kd' and not NOPICKLE: + pickleKDEfile = os.path.join(binp + '/%s_n%d_%s_%d_KDE.pkl' % (name, nsample, key, i + 1)) + if VERBOSE: + print "KDE files will be stored in ", pickleKDEfile + pickle.dump(kde, open(pickleKDEfile, 'wb')) + except (IndexError, TypeError): + pass + #make box_and_whiskers plot + fig = plt.figure(figsize=(8, 15)) + fig.subplots_adjust(bottom=0.18, left=0.18) + ax = fig.add_subplot(111) + plt.grid() + if len(datas) == 0: + continue + + bp = ax.boxplot(datas, patch_artist=True) + for box in bp['boxes']: + box.set(color='#7570b3', linewidth=2) + box.set(facecolor='DarkOrange', alpha=0.4) + for whisker in bp['whiskers']: + whisker.set(color='#7570b3', linewidth=2) + for cap in bp['caps']: + cap.set(color='#7570b3', linewidth=2) + for median in bp['medians']: + median.set(color='k', linewidth=2) + for flier in bp['fliers']: + flier.set(marker='o', color='#7570b3', alpha=0.4) + plt.title("measurement %d: %s" % (i + 1, flux[i]['galnum'])) + plt.xticks(range(1, len(boxlabels) + 1), boxlabels, rotation=90, fontsize=fs - 5) + plt.fill_between(range(1, len(boxlabels) + 1), [8.76] * len(boxlabels), [8.69] * len(boxlabels), facecolor='black', alpha=0.3) + plt.text(1.2, 8.705, "Solar Oxygen Abundance", alpha=0.7) + plt.gca().yaxis.set_major_formatter(FormatStrFormatter('%.2f')) + plt.ylabel('12+log(O/H)', fontsize=fs) + plt.savefig(binp + "/" + name + "_boxplot_n%d_%d.pdf" % (nsample, i + 1), format='pdf') + if ASCIIOUTPUT: + fi.close() + if VERBOSE: + print "uncertainty calculation complete" + + #del datas + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('name', metavar='', type=str, help="the SN file name (root of the _min,_max file names") + parser.add_argument('nsample', metavar='N', type=int, help="number of iterations, minimum 100 (or 0 for no MC sampling)") + parser.add_argument('--clobber', default=False, action='store_true', help="replace existing output") + parser.add_argument('--binmode', default='k', type=str, choices=['d', 's', 'k', 't', 'bb', 'kd'], help='''method to determine bin size + {d: Duanes formula, s: n^1/2, t: 2*n**1/3(default), k: Knuth's rule, + bb: Bayesian blocks, kd: Kernel Density}''') + parser.add_argument('--path', default=None, type=str, help='''input/output path (must contain the input _max.txt and + _min.txt files in a subdirectory sn_data)''') + parser.add_argument('--unpickle', default=False, action='store_true', help="read the pickled realization instead of making a new one") + + parser.add_argument('--verbose', default=False, action='store_true', help="verbose mode") + parser.add_argument('--log', default=None, type=str, help="log file, if not passed defaults to standard output") + parser.add_argument('--nodust', default=False, action='store_true', help=" don't do dust corrections (default is to do it)") + parser.add_argument('--noplot', default=False, action='store_true', help=" don't plot individual distributions (default is to plot all distributions)") + parser.add_argument('--asciiout', default=False, action='store_true', help=" write distribution in an ascii output (default is not to)") + parser.add_argument('--asciidistrib', default=False, action='store_true', help=" write distribution in an ascii output (default is not to)") + parser.add_argument('--md', default='all', type=str, help='''metallicity diagnostic to calculate. + default is 'all', options are: + D02, Z94, M91, C01, P05, M08, M08all, M13, PP04, D13, KD02, DP00 (deprecated), P01''') + parser.add_argument('--multiproc', default=False, action='store_true', help=" multiprocess, with number of threads max(available cores-1, MAXPROCESSES)") + args = parser.parse_args() + + global CLOBBER + global VERBOSE + global BINMODE + global ASCIIOUTPUT + global ASCIIDISTRIB + global NOPLOT + CLOBBER = args.clobber + VERBOSE = args.verbose + BINMODE = args.binmode + NOPLOT = args.noplot + ASCIIOUTPUT = args.asciiout + ASCIIDISTRIB = args.asciidistrib + + if args.unpickle and NOPICKLE: + args.unpickle = False + raw_input("cannot use pickle on this machine, we won't save and won't read saved pickled realizations. Ctr-C to exit, Return to continue?\n") + + if args.path: + path = args.path + else: + assert (os.getenv("MCMetdata")), ''' the _max, _min (and _med) data must live in a folder named sn_data. + pass a path to the sn_data folder, or set up the environmental variable + MCMetdata pointing to the path where sn_data lives ''' + path = os.getenv("MCMetdata") + assert(os.path.isdir(path)), "pass a path or set up the environmental variable MCMetdata pointing to the path where the _min _max _med files live" + if args.nsample == 1: + print "CALCULATING METALLICITY WITHOUT GENERATING MC DISTRIBUTIONS" + if args.nsample == 1 or args.nsample >= 10: + fi = input_data(args.name, path=path) + if fi != -1: + logf = smart_open(args.log) + run(fi, args.nsample, args.md, args.multiproc, logf, unpickle=args.unpickle, dust_corr=(not args.nodust), verbose=VERBOSE) + if args.log: + logf.close() + else: + print "nsample must be at least 100" + + +if __name__ == "__main__": + if PROFILING: + import cProfile + cProfile.run("main()") + else: + main() diff --git a/build/scripts-2.7/metallicity.py b/build/scripts-2.7/metallicity.py new file mode 100755 index 0000000..afc7ec1 --- /dev/null +++ b/build/scripts-2.7/metallicity.py @@ -0,0 +1,245 @@ +############################################################################## +## Calculates oxygen abundance (here called metalicity) based on strong emission lines, +## based on code originally written in IDL by Lisa Kewley (Kewley & Ellison 2008). Outputs +## oxygen abundance in many different diagnostics (see Bianco et al. 2016). +## +##new calculation based on the most recent version of the .pro file. +## +##inputs: +## measured - flux data, must be the format returned by readfile() +## num - number of spectra for which to calculate metallicity, also returned by readfile() +## outfilename - the name of the file the results will be appended to +## red_corr - reddening correction flag - True by default +## disp - if True prints the results, default False +############################################################################## + +import sys +import os +import numpy as np + +IGNOREDUST = False +MP = True +FIXNEGATIVES = True # set to true if no negative flux measurements should be allowed. all negative flux measurements are set to 0 + +##list of metallicity methods, in order calculated +Zs = ["E(B-V)", # based on Halpha, Hbeta + "logR23", # Hbeta, [OII]3727, [OIII]5007, [OIII]4959 + + "D02", # Halpha, [NII]6584 + "Z94", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "M91", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "C01_N2S2", # [OII]3727, [OIII]5007, [NII]6584, [SII]6717 + "C01_R23", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + + "P05", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "P01", # available but deprecated + "PP04_N2Ha", # Halpha, [NII]6584 + "PP04_O3N2", # Halpha, Hbeta,[OIII]5007, [NII]6584 + "DP00", # S23 available but deprecated + "P10_ONS", "P10_ON", + "M08_R23", "M08_N2Ha", "M08_O3Hb", "M08_O2Hb", "M08_O3O2", "M08_O3N2", + "M13_O3N2", "M13_N2", + "D13_N2S2_O3S2", "D13_N2S2_O3Hb", + "D13_N2S2_O3O2", "D13_N2O2_O3S2", + "D13_N2O2_O3Hb", "D13_N2O2_O3O2", + "D13_N2Ha_O3Hb", "D13_N2Ha_O3O2", + "KD02_N2O2", # Halpha, Hbeta, [OII]3727, [NII]6584 + "KD02_N2S2", + "KK04_N2Ha", # Halpha, Hbeta, [OII]3727, [NII]6584 + "KK04_R23", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "KD02comb", + "PM14"] # ,"KK04comb"] +#'KD02_N2O2', 'KD03new_R23', 'M91', 'KD03_N2Ha' + +Zserr = ['PM14err'] # ,"KK04comb"] +#'KD02_N2O2', 'KD03new_R23', 'M91', 'KD03_N2Ha' + + +def get_keys(): + return Zs + + +def get_errkeys(): + return Zserr + + +def printsafemulti(string, logf, nps): + #this is needed because dealing with a log output with multiprocessing + #is painful. but it introduces a bunch of if checks. + #if anyone has a better solution please let me know! + if nps == 1: + print >> logf, string + else: + print string + + +############################################################################## +##fz_roots function as used in the IDL code FED:reference the code here! +############################################################################## + +#@profile +def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, disp=False, verbose=False): + + global IGNOREDUST + mscales.setdustcorrect() + raw_lines = {} + raw_lines['[OIII]5007'] = np.array([float('NaN')]) + raw_lines['Hb'] = np.array([float('NaN')]) + raw_lines['Hz'] = np.array([float('NaN')]) + for k in measured.iterkeys(): + #kills all non-finite terms + measured[k][~(np.isfinite(measured[k][:]))] = 0.0 + if FIXNEGATIVES: + measured[k][measured[k] < 0] = 0.0 + raw_lines[k] = measured[k] + + ######we trust QM better than we trust the measurement of the [OIII]4959 + ######which is typical low S/N so we set it to [OIII]5007/3. + ######change this only if youre spectra are very high SNR + raw_lines['[OIII]4959'] = raw_lines['[OIII]5007'] / 3. + raw_lines['[OIII]49595007'] = raw_lines['[OIII]4959'] + raw_lines['[OIII]5007'] + mscales.setHab(raw_lines['Ha'], raw_lines['Hb']) + + #if Ha or Hb is zero, cannot do red correction + if dust_corr and mscales.hasHa and mscales.hasHb: + with np.errstate(invalid='ignore'): + mscales.calcEB_V() + elif dust_corr and not IGNOREDUST: + + if nps > 1: + print '''WARNING: reddening correction cannot be done + without both H_alpha and H_beta measurement!!''' + + else: + response = raw_input('''WARNING: reddening correction cannot be done without both H_alpha and H_beta measurement!! + Continuing without reddening correction? [Y/n]\n''').lower() + assert(not (response.startswith('n'))), "please fix the input file to include Ha and Hb measurements" + + IGNOREDUST = True + dust_corr = False + mscales.mds['E(B-V)'] = np.ones(len(raw_lines['Ha'])) * 1e-5 + else: + mscales.unsetdustcorrect() + mscales.mds['E(B-V)'] = np.ones(len(raw_lines['Ha'])) * 1e-5 + + for k in ['[OII]3727', '[OIII]5007', '[OI]6300', '[OIII]4959', + '[SII]6717', '[SII]6731', '[SIII]9069', '[SIII]9532' + '[OII]3727', '[OIII]5007', '[OI]6300', '[OIII]4959', + '[NII]6584', '[SIII]9532']: + if k not in raw_lines or (len(raw_lines[k]) == 1 and np.isnan(raw_lines[k][0])): + raw_lines[k] = np.array([0.] * num) + + mscales.setOlines(raw_lines['[OII]3727'], raw_lines['[OIII]5007'], raw_lines['[OI]6300'], raw_lines['[OIII]4959']) + mscales.setSII(raw_lines['[SII]6717'], raw_lines['[SII]6731'], raw_lines['[SIII]9069'], raw_lines['[SIII]9532']) + mscales.setNII(raw_lines['[NII]6584']) + + if mscales.checkminimumreq(dust_corr, IGNOREDUST) == -1: + return -1 + + mscales.calcNIIOII() + mscales.calcNIISII() + + mscales.calcR23() + #mscales.calcS23() + + mscales.initialguess() + mds = mds.split(',') + + + #mscales.printme() + if verbose: + print "calculating metallicity diagnostic scales: ", mds + if 'all' in mds: + mscales.calcD02() + if os.getenv("PYQZ_DIR"): + cmd_folder = os.getenv("PYQZ_DIR") + '/' + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + mscales.calcpyqz() + else: + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable : + export PYQZ_DIR="your/path/where/pyqz/resides/ in bash, for example, if you want this scale. ''', logf, nps) + + mscales.calcZ94() + mscales.calcM91() + + mscales.calcPP04() + + #mscales.calcP05() + mscales.calcP10() + + mscales.calcM08() + mscales.calcM13() + + mscales.calcKD02_N2O2() + mscales.calcKK04_N2Ha() + + mscales.calcKK04_R23() + mscales.calcKDcombined() + + if 'DP00' in mds: + mscales.calcDP00() + if 'P01' in mds: + mscales.calcP01() + + if 'D02' in mds: + mscales.calcD02() + if 'D13' in mds: + if os.getenv("PYQZ_DIR"): + cmd_folder = os.getenv("PYQZ_DIR") + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + #mscales.calcpyqz() + #in order to see the original pyqz plots + #call pyqz with option plot=True by + #using the commented line below instead + mscales.calcpyqz(plot=disp) + else: + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable + PYQZ_DIR if you want this scale. ''', logf, nps) + + if 'D13all' in mds: + if os.getenv("PYQZ_DIR"): + cmd_folder = os.getenv("PYQZ_DIR") + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + #mscales.calcpyqz() + #in order to see the original pyqz plots + #call pyqz with option plot=True by + #using the commented line below instead + mscales.calcpyqz(plot=disp, allD13=True) + else: + printsafemulti('''set path to pyqz as environmental variable +PYQZ_DIR if you want this scale. ''', logf, nps) + + if 'PM14' in mds: + if os.getenv("HIICHI_DIR"): + cmd_folder = os.getenv("HIICHI_DIR") + '/' + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + mscales.calcPM14() + if 'PP04' in mds: + mscales.calcPP04() + if 'Z94' in mds: + mscales.calcZ94() + if 'M91' in mds: + mscales.calcM91() + if 'P10' in mds: + mscales.calcP10() + if 'M13' in mds: + mscales.calcM13() + if 'M08all' in mds: + mscales.calcM08(allM08=True) + elif 'M08' in mds: + mscales.calcM08() + if 'P05' in mds: + mscales.calcP05() + if 'C01' in mds: + mscales.calcC01_ZR23() + if 'KD02' in mds: + mscales.calcKD02_N2O2() + mscales.calcKK04_N2Ha() + mscales.calcKK04_R23() + mscales.calcKDcombined() diff --git a/build/scripts-2.7/metscales.py b/build/scripts-2.7/metscales.py new file mode 100755 index 0000000..bb059ed --- /dev/null +++ b/build/scripts-2.7/metscales.py @@ -0,0 +1,1273 @@ +import numpy as np +#import sys +import scipy.stats as stats +import numpy.polynomial.polynomial as nppoly +from metallicity import get_keys, printsafemulti + +niter = 5 # number of iteations+1 for KD02 methods + +k_Ha = 2.535 # CCM Rv=3.1 +k_Hb = 3.609 # CCM Rv=3.1 + +#k_O1=2.661 # CCM Rv=3.1 +k_O2 = 4.771 # CCM Rv=3.1 +k_O35007 = 3.341 # CCM Rv=3.1 +k_O34959 = 3.384 # CCM Rv=3.1 +k_O3 = (k_O35007 + k_O34959) / 2. + +k_N2 = 2.443 # CCM Rv=3.1 +k_S2 = 2.381 # CCM Rv=3.1 + +k_S3 = 1 # guess for CCM Rv=3.1 + +global DUSTCORRECT +DUSTCORRECT = True + +''' +R23_coef=np.zeros((5,7)) # coefficients from model grid fits +R23c0=[-3267,-3727.42,-4282.30,-4745.18,-4516.46,-3509.63,-1550.53] +R23_coef[:,0]=[-3267.93,1611.04,-298.187,24.5508,-0.758310] # q=5e6 +R23_coef[:,1]=[-3727.42,1827.45,-336.340,27.5367,-0.845876] # q=1e7 +R23_coef[:,2]=[-4282.30,2090.55,-383.039,31.2159,-0.954473] # q=2e7 +R23_coef[:,3]=[-4745.18,2309.42,-421.778,34.2598,-1.04411] # q=4e7 +R23_coef[:,4]=[-4516.46,2199.09,-401.868,32.6686,-0.996645] # q=8e7 +R23_coef[:,5]=[-3509.63,1718.64,-316.057,25.8717,-0.795242] # q=1.5e8 +R23_coef[:,6]=[-1550.53,784.262,-149.245,12.6618,-0.403774] # q=3e8 + + +N2S2_coef=np.zeros((5,7)) # coefficients from model grid fits +N2S2c0=[-1042.47,-1879.46,-2027.82,-2080.31,-2162.93,-2368.56,-2910.63] +N2S2_coef[:,0]=[-1042.47,521.076,-97.1578,8.00058,-0.245356] +N2S2_coef[:,1]=[-1879.46,918.362,-167.764,13.5700,-0.409872] +N2S2_coef[:,2]=[-2027.82,988.218,-180.097,14.5377,-0.438345] +N2S2_coef[:,3]=[-2080.31,1012.26,-184.215,14.8502,-0.447182] +N2S2_coef[:,4]=[-2162.93,1048.97,-190.260,15.2859,-0.458717] +N2S2_coef[:,5]=[-2368.56,1141.97,-205.908,16.4451,-0.490553] +N2S2_coef[:,6]=[-2910.63,1392.18,-249.012,19.7280,-0.583763] + +O3O2_coef=np.zeros((4,8)) # coefficients from model grid fits +O3O2c0=[-36.9772,-74.2814,-36.7948,-81.1880,-52.6367,-86.8674,-24.4044,49.4728] +O3O2_coef[:,0]=[-36.9772,10.2838,-0.957421,0.0328614] #z=0.05 +O3O2_coef[:,1]=[-74.2814,24.6206,-2.79194,0.110773] # z=0.1 +O3O2_coef[:,2]=[-36.7948,10.0581,-0.914212,0.0300472] # z=0.2 +O3O2_coef[:,3]=[-81.1880,27.5082,-3.19126,0.128252] # z=0.5 +O3O2_coef[:,4]=[-52.6367,16.0880,-1.67443,0.0608004] # z=1.0 +O3O2_coef[:,5]=[-86.8674,28.0455,-3.01747,0.108311] # z=1.5 +O3O2_coef[:,6]=[-24.4044,2.51913,0.452486,-0.0491711] # z=2.0 +O3O2_coef[:,7]=[49.4728,-27.4711,4.50304,-0.232228] # z=3.0 +''' + +M08_coefs = {'R23': [0.7462, -0.7149, -0.9401, -0.6154, -0.2524], + 'N2Ha': [-0.7732, 1.2357, -0.2811, -0.7201, -0.3330], + 'O3Hb': [0.1549, -1.5031, -0.9790, -0.0297], + 'O3O2': [-0.2839, -1.3881, -0.3172], + 'O2Hb': [0.5603, 0.0450, -1.8017, -1.8434, -0.6549], + 'O3N2': [0.4520, -2.6096, -0.7170, 0.1347]} + +#this is to check the Maiolino coefficients and find the split maximum of the cirves with degeneracy +''' +import pylab as pl + +x=np.arange(7.0,9.5,0.1) + +for k in M08_coefs.iterkeys(): + print k,max(nppoly.polyval(x-8.69,M08_coefs[k])),x[nppoly.polyval(x-8.69,M08_coefs[k])==max(nppoly.polyval(x-8.69,M08_coefs[k]))] + pl.plot(x,nppoly.polyval(x-8.69,M08_coefs[k]), label=k+' max:%.1f'%x[nppoly.polyval(x-8.69,M08_coefs[k])==max(nppoly.polyval(x-8.69,M08_coefs[k]))]) + +print nppoly.polyval(8.4-8.69,M08_coefs['N2Ha']) +pl.ylabel("log R") +pl.xlabel("12+log(O/H)") +pl.legend(loc=3) +pl.show() +''' + + +class diagnostics: + def __init__(self, num, logf, nps): + self.nm = num + self.Ha = None + self.Hb = None + + self.hasHa, self.hasHb = False, False + self.hasO2, self.hasO3 = False, False + self.hasS2, self.hasN2 = False, False + + self.hasO3Hb = False + self.hasO3O2 = False + + self.hasN2O2 = False + self.hasN2S2 = False + + self.hasS26731 = False + self.hasS39532 = False + self.hasS39069 = False + self.hasS2Hb = False + + self.N2O2_roots = None + #other lines calculated and repeatedly used + self.P = None + self.R2 = None + self.R3 = None + self.R23 = None + self.S2Hb = None + self.N2 = None + self.N2S2 = None + self.O23727 = None + self.O35007 = None + self.N26584 = None + self.S26717 = None + self.S26731 = None + self.S39069 = None + self.S39532 = None + + self.O34959p5007 = None + self.O35007O2 = None + self.O2O35007 = None + + self.logR23 = None + self.logN2O2 = None + self.logN2S2 = None + self.logO3O2 = None + self.logS23 = None + #self.logS3S2=None + + self.logO2Hb = None + self.logO3Hb = None + self.logN2Ha = None + self.logS2Ha = None + + self.logO35007O2 = None + self.logO2O35007 = None + + self.logO3O2sq = None + self.logq = None + self.Z_init_guess = None + self.N2O2_coef0 = 1106.8660 + + self.OIII_OII = None + self.OIII_Hb = None + self.OIII_SII = None + + self.NII_OII = None + self.NII_SII = None + #metallicity diagnostics to be returned + + self.mds = {} + for Z in get_keys(): + self.mds[Z] = None + + #setting output file + self.logf = logf + self.nps = nps + + def printme(self, verbose=False): + try: + print "\nHa", np.mean(self.Ha) + if verbose: + print self.Ha + except (IndexError, TypeError): + pass + try: + print "\nHb", np.mean(self.Hb) + if verbose: + print self.Hb + except (IndexError, TypeError): + pass + try: + print "\nO2", np.mean(self.O23727) + if verbose: + print self.O23727 + except (IndexError, TypeError): + pass + try: + print "\nO3", np.mean(self.O35007) + if verbose: + print self.O35007 + except (IndexError, TypeError): + pass + try: + print "\nO34959", np.mean(self.O34959) + if verbose: + print self.O34959 + except (IndexError, TypeError): + pass + try: + print "\nZ94", np.mean(self.mds['Z94']) + if verbose: + print self.mds['Z94'] + except (IndexError, TypeError): + pass + try: + print "\nR23", np.mean(self.R23) + if verbose: + print self.R23 + except (IndexError, TypeError): + pass + try: + print "\nlog(R23)", np.mean(self.logR23) + if verbose: + print self.logR23 + except (TypeError, IndexError): + pass + try: + print "\nlog([NII][OII])", stats.nanmean(self.logN2O2) + if verbose: + print self.logN2O2 + except (TypeError, IndexError): + pass + try: + print "\nlog([OIII][OII])", stats.nanmean(self.logO3O2) + if verbose: + print self.logO3O2 + except (TypeError, IndexError): + pass + for k in self.mds.iterkeys(): + print "\n", k, + try: + print stats.nanmean(self.mds[k]), np.stdev(self.mds[k]) + except (IndexError, TypeError): + if verbose: + print self.mds[k] + + def checkminimumreq(self, red_corr, ignoredust): + if red_corr and not ignoredust: + if not self.hasHa: + return -1 + if not self.hasHb: + return -1 + #if not self.hasO2 : + # return -1 + #if not self.hasO3 : + # return -1 + #if not self.hasS2 : + # return -1 + if not self.hasN2 and not (self.hasO2 and self.hasO3): + return -1 + + def fz_roots(self, coef): + if len(coef.shape) == 1: + coef[~(np.isfinite(coef))] = 0.0 + rts = np.roots(coef[::-1]) + if rts.size == 0: + printsafemulti('WARNING: fz_roots failed', self.logf, self.nps) + rts = np.zeros(coef.size - 1) + return rts + + else: + rts = np.zeros((coef.shape[0], coef.shape[1] - 1), dtype=complex) + coef[~(np.isfinite(coef))] = 0.0 + + for i in range(coef.shape[0]): + rts[i] = np.roots(coef[i][::-1]) # ::-1][0]) + return rts + + def setdustcorrect(self): + global DUSTCORRECT + DUSTCORRECT = True + + def unsetdustcorrect(self): + global DUSTCORRECT + DUSTCORRECT = False + + def dustcorrect(self, l1, l2, flux=False): + #global DUSTCORRECT + if DUSTCORRECT: + if not flux: + return 0.4 * self.mds['E(B-V)'] * (l1 - l2) + return 10 ** (0.4 * self.mds['E(B-V)'] * (l1 - l2)) + else: + if not flux: + return 0 + return 1.0 + + def setHab(self, Ha, Hb): + self.Ha = Ha + self.Hb = Hb + if sum(self.Ha > 0): + self.hasHa = True + if sum(self.Hb > 0): + self.hasHb = True + + def setOlines(self, O23727, O35007, O16300, O34959): + self.O23727 = O23727 + self.O35007 = O35007 + self.O16300 = O16300 + + if sum(self.O35007 > 0): + self.hasO3 = True + if sum(self.O23727 > 0): + self.hasO2 = True + + if self.hasO2 and self.hasO3: + self.O35007O2 = (self.O35007 / self.O23727) * self.dustcorrect(k_O3, k_O2, flux=True) + self.O2O35007 = (self.O23727 / self.O35007) * self.dustcorrect(k_O2, k_O3, flux=True) + + self.logO35007O2 = np.log10(self.O35007O2) + self.logO2O35007 = np.log10(self.O2O35007) + + #self.logO2O35007Hb=np.log10((self.O23727+self.O35007)/self.Hb) + # ratios for other diagnostics - slightly different ratios needed + if self.hasHb: + self.logO2O35007Hb = np.log10((self.O23727 / self.Hb) * self.dustcorrect(k_O2, k_Hb, flux=True)) + \ + (self.O35007 / self.Hb) * self.dustcorrect(k_O35007, k_Hb, flux=True) + + else: + printsafemulti("WARNING: needs O lines and and Ha/b: did you run setHab()?", self.logf, self.nps) + if self.hasHb: + if self.hasO2: + self.logO2Hb = np.log10(self.O23727 / self.Hb) + self.dustcorrect(k_O2, k_Hb) # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + if self.hasO3: + self.O3Hb = (self.O35007 / self.Hb) + self.dustcorrect(k_O35007, k_Hb, flux=True) # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + self.logO3Hb = np.log10(self.O3Hb) + self.hasO3Hb = True + + + if self.hasO2 and self.hasO3: + self.OIII_OII = np.log10(self.O35007 / self.O23727 + self.dustcorrect(k_O35007, k_O2, flux=True)) + if O34959 is not None and sum(O34959 > 0) > 0: + self.O34959p5007 = (O34959 + self.O35007) + self.logO3O2 = np.log10((self.O34959p5007) / self.O23727) + self.dustcorrect(k_O3, k_O2) + #this is useful when we get logq + self.hasO3O2 = True + if self.hasHb: + self.OIII_Hb = np.log10(self.O35007 / self.Hb + self.dustcorrect(k_O35007, k_Hb, flux=True)) + + def setNII(self, N26584): + if N26584 is not None and sum(N26584 > 0): + self.N26584 = N26584 + self.hasN2 = True + if self.hasHa: + self.logN2Ha = np.log10(self.N26584 / self.Ha) # +self.dustcorrect(k_N2,k_Ha,flux=True) + #lines are very close: no dust correction + #Note: no dust correction cause the lies are really close! + else: + printsafemulti("WARNING: needs NII6584 and Ha to calculate NIIHa: did you run setHab()?", self.logf, self.nps) + if self.hasS2 and self.hasS26731 and self.hasN2: + self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_S2,flux=True) + #lines are very close: no dust correction + if self.hasO2 and self.hasN2: + self.NII_OII = np.log10(self.N26584 / self.O23727 + self.dustcorrect(k_N2, k_O2, flux=True)) + + def setSII(self, S26717, S26731, S39069, S39532): + if S26717 is not None and sum(S26717 > 0) > 0: + self.S26717 = S26717 + self.hasS2 = True + + if self.hasHa: + self.logS2Ha = np.log10(self.S26717 / self.Ha) + self.dustcorrect(k_S2, k_Ha) + else: + printsafemulti("WARNING: needs SII6717 and Ha to calculate SIIHa: did you run setHab() and setS()?", self.logf, self.nps) + if S26731 is not None and sum(S26731 > 1e-9) > 0: + self.S26731 = S26731 + self.hasS26731 = True + if S39069 is not None and sum(S39069 > 1e-9) > 0: + self.S39069 = S39069 + self.hasS39069 = True + if S39532 is not None and sum(S39532 > 1e-9) > 0: + self.S39532 = S39532 + self.hasS39532 = True + if self.hasS2: + if self.hasN2 and self.NII_SII is None and self.hasS26731: + self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_O2,flux=True) + #lines are very close: no dust correction + if self.hasO3 and self.OIII_SII is None and self.hasS26731: + self.OIII_SII = np.log10(self.O35007 / (self.S26717 + self.S26731) + self.dustcorrect(k_O3, k_S2, flux=True)) + + #@profile + def calcEB_V(self): + printsafemulti("calculating E(B-V)", self.logf, self.nps) + self.mds['E(B-V)'] = np.log10(2.86 * self.Hb / self.Ha) / (0.4 * (k_Ha - k_Hb)) # E(B-V) + self.mds['E(B-V)'][self.mds['E(B-V)'] <= 0] = 1e-5 + + #@profile + def calcNIISII(self): + if self.hasS2 and self.hasN2: + self.N2S2 = self.N26584 / self.S26717 + self.dustcorrect(k_N2, k_S2, flux=True) # 0.4*self.mds['E(B-V)']*(k_N2-k_S2) + self.logN2S2 = np.log10(self.N26584 / self.S26717) + self.dustcorrect(k_N2, k_S2) # 0.4*self.mds['E(B-V)']*(k_N2-k_S2) + self.hasN2S2 = True + else: + printsafemulti("WARNING: needs SII6717 and NII6584 to calculate NIISII: did you run setN2() and setS?", self.logf, self.nps) + + #@profile + def calcNIIOII(self): + if self.hasN2 and self.hasO2: + self.logN2O2 = np.log10(self.N26584 / self.O23727) + self.dustcorrect(k_N2, k_O2) + self.hasN2O2 = True + if not self.hasN2O2 or np.mean(self.logN2O2) < 1.2: + + try: + printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods should only be used for log([NII]6564/[OII]3727) >1.2, + the mean log([NII]6564/[OII]3727)= %f''' % np.mean(self.logN2O2), self.logf, self.nps) + except TypeError: + printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods + should only be used for log([NII]6564/[OII]3727) >1.2, + the mean log([NII]6564/[OII]3727)= %s''' % self.logN2O2, self.logf, self.nps) + + if not self.hasN2O2: + self.N2O2_roots = np.zeros(self.nm) + float('NaN') + else: + N2O2_coef = np.array([[self.N2O2_coef0, -532.15451, 96.373260, -7.8106123, 0.23928247]] * self.nm).T # q=2e7 line (approx average) + N2O2_coef[0] -= self.logN2O2 + N2O2_coef = N2O2_coef.T + # finding roots for == (4) + self.N2O2_roots = np.array([self.fz_roots(N2O2_coef)])[0] + + #@profile + def calcR23(self): + printsafemulti("calculating R23", self.logf, self.nps) + + #R23 NEW Comb, [NII]/Ha: KK04 = Kobulnicky & Kewley, 2004, submitted' + if self.hasO3 and self.hasO2 and self.hasHb: + self.R2 = (self.O23727 / self.Hb) * self.dustcorrect(k_O2, k_Hb, flux=True) + self.R3 = (self.O34959p5007 / self.Hb) * self.dustcorrect(k_O3, k_Hb, flux=True) + self.R23 = self.R2 + self.R3 + self.logR23 = np.log10(self.R23) + self.mds['logR23'] = self.logR23 + #note that values of logR23 > 0.95 are unphysical. + #you may choose to uncomment the line below + #self.logR23[self.logR23>0.95]=0.95 + else: + printsafemulti("WARNING: need O3, O2, Hb", self.logf, self.nps) + + #@profile + def calcS23(self): + printsafemulti("calculating S23", self.logf, self.nps) + #the original code here uses S267176731, + #which is however set to 6717 as default + #Vilchez & Esteban (1996) + if self.hasS2: + if self.hasS39069 and self.hasHb: + self.logS23 = np.log10((self.S26717 / self.Hb) * + self.dustcorrect(k_S2, k_Hb, flux=True) + + (self.S39069 / self.Hb) * + self.dustcorrect(k_S3, k_Hb, flux=True)) + + #self.logS3S2=np.log10(S39069/self.S26717)+self.dustcorrect(k_S3,k_S2) + + ##@profile + def calclogq(self, Z): + if not self.hasO3O2: + printsafemulti("WARNING: needs O3,O2,Hb to calculate logq properly.", self.logf, self.nps) + return -1 + if self.logO3O2sq is None: + self.logO3O2sq = self.logO3O2 ** 2 + return (32.81 - 1.153 * self.logO3O2sq + Z * (-3.396 - 0.025 * self.logO3O2 + 0.1444 * self.logO3O2sq)) / (4.603 - 0.3119 * self.logO3O2 - \ + 0.163 * self.logO3O2sq + Z * (-0.48 + 0.0271 * self.logO3O2 + 0.02037 * self.logO3O2sq)) + + ##@profile + def initialguess(self): + # Initial Guess - appearing in LK code as of Nov 2006 + # upper branch: if no lines are available, metallicity is set to 8.7 + self.Z_init_guess = np.zeros(self.nm) + 8.7 + # use [N2]/Ha + if self.hasHa and self.hasN2: + self.Z_init_guess[(self.logN2Ha < -1.3) & (self.N26584 != 0.0)] = 8.2 + self.Z_init_guess[(self.logN2Ha < -1.1) & (self.logN2Ha >= -1.3) & (self.N26584 != 0.0)] = 8.4 + #A1 KE08 + self.Z_init_guess[(self.logN2Ha >= -1.1) & (self.N26584 != 0.0)] = 8.7 + #use [N2]/[O2] + if self.hasN2 and self.hasO2: + N2O2 = np.zeros(self.nm) + float('nan') + if self.hasHb: + ###FED CHECK THIS! + N2O2 = self.N26584 * self.Ha * self.Hb * self.O23727 + if not self.hasN2O2: + printsafemulti("WARNING: must calculate logN2O2 first", self.logf, self.nps) + self.calcNIIOII() + self.Z_init_guess[(self.logN2O2 < -1.2) & (N2O2 != 0.0)] = 8.2 + # at logN2O2<-1.2 using low-Z gals, A1 KE08 + self.Z_init_guess[(self.logN2O2 >= -1.2) & (N2O2 != 0.0)] = 8.7 + + # at logN2O2>-1.2 using HII regions + + + + #######################these are the metallicity diagnostics################## + #@profile + def calcpyqz(self, plot=False, allD13=False): + printsafemulti("calculating D13", self.logf, self.nps) + + # initializing variable pyqz to avoid style issues + # (pyqz not defined is reported as error by Landscape.io w/ import in func + pyqz = None + try: + import pyqz + except ImportError: + return -1 + + #check version of pyqz + from distutils.version import StrictVersion + oldpyqz = False + if StrictVersion(pyqz.__version__) <= StrictVersion('0.5.0'): + oldpyqz = True + + if self.NII_SII is not None and allD13: + if self.OIII_SII is not None: + + if oldpyqz: + self.mds['D13_N2S2_O3S2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_SII]), 'NII/SII', 'OIII/SII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + #pyqz.get_grid_fn(Pk=5.0,calibs='GCZO', kappa =20, struct='pp') + self.mds['D13_N2S2_O3S2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_SII])], \ + '[NII]/[SII]+;[OIII]/[SII]+', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + if self.OIII_Hb is not None: + if oldpyqz: + self.mds['D13_N2S2_O3Hb'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_Hb]), 'NII/SII', 'OIII/Hb', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2S2_O3SHb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/[SII]+;[OIII]/Hb', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + + + if self.OIII_OII is not None: + if oldpyqz: + self.mds['D13_N2S2_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_OII]), 'NII/SII', 'OIII/OII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2S2_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_OII])], \ + '[NII]/[SII]+;[OIII]/[OII]+', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + + if self.NII_OII is not None and allD13: + if self.OIII_SII is not None: + if oldpyqz: + self.mds['D13_N2O2_O3S2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_SII]), 'NII/OII', 'OIII/SII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2O2_O3S2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_SII])], \ + '[NII]/[OII]+;[OIII]/[SII]+', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + + if self.OIII_Hb is not None: + if oldpyqz: + self.mds['D13_N2O2_O3Hb'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_Hb]), 'NII/OII', 'OIII/Hb', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2O2_O3Hb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/[OII]+;[OIII]/Hb', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + if self.OIII_OII is not None: + if oldpyqz: + self.mds['D13_N2O2_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_OII]), 'NII/OII', 'OIII/OII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2O2_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_OII])], \ + '[NII]/[OII]+;[OIII]/[OII]+', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + if self.logN2Ha is not None: + if self.OIII_Hb is not None: + if oldpyqz: + self.mds['D13_N2Ha_O3Hb'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_Hb]), 'NII/Ha', 'OIII/Hb', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2Ha_O3Hb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/Ha;[OIII]/Hb', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + if self.OIII_OII is not None: + if oldpyqz: + self.mds['D13_N2Ha_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_OII]), 'NII/Ha', 'OIII/OII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + + else: + self.mds['D13_N2Ha_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/Ha;[OIII]/[OII]+', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + #@profile + def calcDP00(self): + # Diaz, A. I., & Perez-Montero, E. 2000, MNRAS, 312, 130 + # As per KD02: DP00 diagnostic systematically underestimates the + # abundance relative to the comparison abundance. + # A term is added to improve the fit according to KD02 Eq. 6 + # AVAILABLE BUT DEPRECATED + printsafemulti("calculating DP00", self.logf, self.nps) + + if self.logS23 is None: + self.calcS23() + if self.logS23 is None: + printsafemulti("WARNING: Cannot compute this without S23", self.logf, self.nps) + return -1 + self.mds['DP00'] = 1.53 * self.logS23 + 8.27 + 1.0 / (2.0 - 9.0 * self.logS23 ** 3) + + #@profile + def calcD02(self): + # [NII]/Ha Denicolo, Terlevich & Terlevich (2002), MNRAS, 330, 69 + #FED:added uncertainties + printsafemulti("calculating D02", self.logf, self.nps) + + e1 = np.random.normal(0, 0.05, self.nm) + e2 = np.random.normal(0, 0.1, self.nm) + if self.hasN2 and self.hasHa: + self.mds['D02'] = 9.12 + e1 + (0.73 + e2) * self.logN2Ha + else: + printsafemulti("WARNING: need N2Ha to do this. did you run setHab and setNII", self.logf, self.nps) + + #@profile + def calcPP04(self): + ### PP04_N2_Z, PP04_O3N2_Z Pettini & Pagel diagnostics - + ### Pettini & Pagel (2004), MNRAS, 348, L59 + # [NII]/Ha Pettini & Pagel (2004), MNRAS, 348, L59 + #discriminating lower and upper branch using [NII]/[OII] or [NII]/Ha + printsafemulti("calculating PP04", self.logf, self.nps) + if self.hasN2 and self.hasHa: + self.mds['PP04_N2Ha'] = nppoly.polyval(self.logN2Ha, [9.37, 2.03, 1.26, 0.32]) + + #FED: restricting the range as per paper + index = (self.logN2Ha > -2.5) * (self.logN2Ha < -0.3) + self.mds['PP04_N2Ha'][~index] = float('NaN') + if self.hasO3Hb: + self.mds['PP04_O3N2'] = 8.73 - 0.32 * (self.logO3Hb - self.logN2Ha) + index = (self.logO3Hb > 2) + self.mds['PP04_O3N2'][index] = float('NaN') + else: + printsafemulti("WARNING: need O3Hb for PP04_O3N2", self.logf, self.nps) + else: + printsafemulti("WARNING: need N2Ha to do this. did you run setHab and setNII", self.logf, self.nps) + + #@profile + def calcZ94(self): + ### calculating z from Kobulnicky,Kennicutt,Pizagno (1998) + ### parameterization of Zaritzky et al. (1994) + ###Z94 = Zaritsky, D., Kennicutt, R. C., & Huchra, J. P. 1994, + ###ApJ, 420, 87 + ### only valid on the upper branch of R23 (KE08 A2.4) + + printsafemulti("calculating Z94", self.logf, self.nps) + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + self.mds['Z94'] = nppoly.polyval(self.logR23, [9.265, -0.33, -0.202, -0.207, -0.333]) + self.mds['Z94'][(self.logR23 > 0.9)] = None + ## 0.9 is a conservative constraint to make sure that we are + ## only using the upper branch (i.e. 12+log(O/H)>8.4) + + def calcP(self): + if self.P is None: + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + #R3=10**self.logO349595007Hb + #R2=10**self.logO2Hb + #P = R3/(R2+R3) + self.P = self.R3 / self.R23 + + #@profile + def calcP05(self): + # #### P-method ##### + ##Pilyugin+ 2005 method. Based on [OIII],[OII], Hbeta + ##calibrated from Te method + # make sure you run setOlines() first + printsafemulti("calculating P05", self.logf, self.nps) + + if self.calcP() == -1: + return -1 + if self.Z_init_guess is None: + self.initialguess() + + Psq = self.P * self.P + + P_abund_up = (self.R23 + 726.1 + 842.2 * self.P + 337.5 * Psq) / (85.96 + 82.76 * self.P + 43.98 * Psq + 1.793 * self.R23) + P_abund_low = (self.R23 + 106.4 + 106.8 * self.P - 3.40 * Psq) / (17.72 + 6.60 * self.P + 6.95 * Psq - 0.302 * self.R23) + + self.mds['P05'] = P_abund_up + self.mds['P05'][self.Z_init_guess < 8.4] = P_abund_low[self.Z_init_guess < 8.4] + + #@profile + def calcP10(self): + # #### P-method ##### + ##Pilyugin+ 2010 method. + ##calibrated from Te method + # need Hb + #The Astrophysical Journal, Volume 720, Issue 2, pp. 1738-1751 (2010). + #Published in Sep 2010 + + printsafemulti("calculating P10", self.logf, self.nps) + + if not self.hasHb: + printsafemulti("this method needs Hb", self.logf, self.nps) + return -1 + self.mds['P10_ONS'] = np.zeros(self.nm) + float('NaN') + self.mds['P10_ON'] = np.zeros(self.nm) + float('NaN') + #P10N2=np.zeros(self.nm)+float('NaN') + #P10S2=np.zeros(self.nm)+float('NaN') + P10logR3 = np.zeros(self.nm) + float('NaN') + P10logR2 = np.zeros(self.nm) + float('NaN') + P10logN2 = np.zeros(self.nm) + float('NaN') + P10logS2 = np.zeros(self.nm) + float('NaN') + + self.calcP() + if self.R2 is not None: + P10logR2 = np.log(self.R2) + + if self.R3 is not None: + P10logR3 = np.log(self.R3) + + if self.hasN2: + #the ratio of N26548 and N26548 is N26584/N26548 = 3 + #independent on physical conditions + #The Physics and Dynamics of Planetary Nebulae + # By Grigor A. Gurzadyan + P10logN2 = np.log((self.N26584 * 1.33) / self.Hb) + self.dustcorrect(k_N2, k_Hb) + + if self.hasS2 and self.hasS26731: + self.S2Hb = ((self.S26717 + self.S26731) / self.Hb) + self.dustcorrect(k_S2, k_Hb, flux=True) + self.hasS2Hb = True + P10logS2 = np.log10(self.S2Hb) + + P10logN2S2 = P10logN2 - P10logS2 + P10logN2R2 = P10logN2 - P10logR2 + P10logS2R2 = P10logS2 - P10logR2 + + coefsONS0 = np.array([8.277, 0.657, -0.399, -0.061, 0.005]) + coefsONS1 = np.array([8.816, -0.733, 0.454, 0.710, -0.337]) + coefsONS2 = np.array([8.774, -1.855, 1.517, 0.304, 0.328]) + + vsONS = np.array([np.ones(self.nm), self.P, P10logR3, P10logN2R2, P10logS2R2]).T + + coefsON0 = np.array([8.606, -0.105, -0.410, -0.150]) + coefsON1 = np.array([8.642, 0.077, 0.411, 0.601]) + coefsON2 = np.array([8.013, 0.905, 0.602, 0.751]) + + vsON = np.array([np.ones(self.nm), P10logR3, P10logR2, P10logN2R2]).T + + indx = P10logN2 > -0.1 + if self.P is not None: + self.mds['P10_ONS'][indx] = np.dot(vsONS[indx], coefsONS0) + self.mds['P10_ON'][indx] = np.dot(vsON[indx], coefsON0) + + indx = (P10logN2 < -0.1) * (P10logN2S2 > -0.25) + if self.P is not None: + self.mds['P10_ONS'][indx] = np.dot(vsONS[indx], coefsONS1) + self.mds['P10_ON'][indx] = np.dot(vsON[indx], coefsON1) + + indx = (P10logN2 < -0.1) * (P10logN2S2 < -0.25) + if self.P is not None: + self.mds['P10_ONS'][indx] = np.dot(vsONS[indx], coefsONS2) + self.mds['P10_ON'][indx] = np.dot(vsON[indx], coefsON2) + + indx = ~((self.mds['P10_ONS'] > 7.1) * (self.mds['P10_ON'] > 7.1) * (self.mds['P10_ONS'] < 9.4) * (self.mds['P10_ON'] < 9.4)) + if self.P is not None: + self.mds['P10_ONS'][indx] = float('NaN') + self.mds['P10_ON'][indx] = float('NaN') + + #@profile + def calcP01(self): + # P-method 2001 upper branch (derecated and commented out) + # Pilyugin 2001 + # available but deprecated + printsafemulti("calculating old P05", self.logf, self.nps) + + if self.Z_init_guess is None: + self.initialguess() + if self.hasO3O2 and self.hasO3 and self.hasO2: + P = 10 ** self.logO3O2 / (1 + 10 ** self.logO3O2) + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + Psq = P ** 2 + P_abund_old = (self.R23 + 54.2 + 59.45 * P + 7.31 * Psq) / (6.07 + 6.71 * P + 0.371 * Psq + 0.243 * self.R23) + self.mds['P01'] = np.zeros(self.nm) + float('NaN') + self.mds['P01'][self.Z_init_guess >= 8.4] = P_abund_old[self.Z_init_guess >= 8.4] + else: + printsafemulti("WARNING: need OIIIOII to calculate P01, did you set them up with setOlines()?", self.logf, self.nps) + + #@profile + def calcC01_ZR23(self): + # C01 = Charlot, S., & Longhetti, M., 2001, MNRAS, 323, 887 + # Charlot 01 R23 calibration: (case F) ## + # available but deprecated + printsafemulti("calculating C01", self.logf, self.nps) + + if self.hasO3 and self.hasO2 and self.hasO3Hb: + x2 = self.O2O35007 / 1.5 + x3 = (10 ** self.logO3Hb) * 0.5 + self.mds['C01_R23'] = np.zeros(self.nm) + float('NaN') + self.mds['C01_R23'][self.O2O35007 < 0.8] = np.log10(3.78e-4 * (x2[self.O2O35007 < 0.8]) ** 0.17 * x3[self.O2O35007 < 0.8] ** (-0.44)) + 12.0 + + self.mds['C01_R23'][self.O2O35007 >= 0.8] = np.log10(3.96e-4 * x3[self.O2O35007 >= 0.8] ** (-0.46)) + 12.0 + else: + printsafemulti('''WARNING: need [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, +did you set them up with setOlines()?''', self.logf, self.nps) + + # Charlot 01 calibration: (case A) based on [N2]/[SII]## + # available but deprecated + if not self.hasN2S2: + printsafemulti("WARNING: trying to calculate logNIISII", self.logf, self.nps) + self.calcNIISII() + if self.hasN2S2 and self.hasO3 and self.hasO2 and self.hasO3Hb: + self.mds['C01_N2S2'] = np.log10(5.09e-4 * (x2 ** 0.17) * ((self.N2S2 / 0.85) ** 1.17)) + 12 + else: + printsafemulti('''WARNING: needs [NII]6584, [SII]6717, [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, +did you set them up with setOlines() and ?''', self.logf, self.nps) + + #@profile + def calcM91(self): + # ## calculating McGaugh (1991) + # McGaugh, S.S., 1991, ApJ, 380, 140' + # M91 calibration using [N2O2] as + # initial estimate of abundance: + # this initial estimate can be + # changed by replacing + # OH_init by another guess, eg C01_Z + # NOTE: occasionally the M91 + # 'upper branch' will give a metallicity + # that is lower than the 'lower branch'. + # Happens for very high R23 values. + # If R23 is higher than the intersection + # (calculate the intersection), then + # the metallicity is likely to be around + # the R23 maximum = 8.4 + + printsafemulti("calculating M91", self.logf, self.nps) + self.mds['M91'] = np.zeros(self.nm) + float('NaN') + + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + + if self.Z_init_guess is None: + self.initialguess() + + M91_Z_low = nppoly.polyval(self.logR23, [12.0 - 4.944, 0.767, 0.602]) - \ + self.logO3O2 * nppoly.polyval(self.logR23, [0.29, 0.332, -0.331]) + M91_Z_up = nppoly.polyval(self.logR23, [12.0 - 2.939, -0.2, -0.237, -0.305, -0.0283]) - \ + self.logO3O2 * nppoly.polyval(self.logR23, [0.0047, -0.0221, -0.102, -0.0817, -0.00717]) + + indx = (np.abs(self.logO3O2) > 0) * (np.abs(self.logR23) > 0) * (self.Z_init_guess < 8.4) + self.mds['M91'][indx] = M91_Z_low[indx] + indx = (np.abs(self.logO3O2) > 0) * (np.abs(self.logR23) > 0) * (self.Z_init_guess >= 8.4) + self.mds['M91'][indx] = M91_Z_up[indx] + self.mds['M91'][(M91_Z_up < M91_Z_low)] = float('NaN') + + #@profile + def calcM13(self): + #Marino+ 2013 + printsafemulti("calculating M13", self.logf, self.nps) + + if not self.hasHa or not self.hasN2: + printsafemulti("WARNING: need O3, N2, Ha and Hb, or at least N2 and Ha", self.logf, self.nps) + return -1 + else: + e1 = np.random.normal(0, 0.027, self.nm) + e2 = np.random.normal(0, 0.024, self.nm) + self.mds["M13_N2"] = 8.743 + e1 + (0.462 + e2) * self.logN2Ha + if self.hasHb and self.hasO3: + e1 = np.random.normal(0, 0.012, self.nm) + e2 = np.random.normal(0, 0.012, self.nm) + O3N2 = self.logO3Hb - self.logN2Ha + self.mds["M13_O3N2"] = 8.533 + e1 - (0.214 + e1) * O3N2 + index = (self.logO3Hb > 1.7) + self.mds["M13_O3N2"][index] = float('NaN') + + #@profile + def calcM08(self, allM08=False): + #Maiolino+ 2008 + #Astronomy and Astrophysics, Volume 488, Issue 2, 2008, pp.463-479 + #Published in Sep 2008 + printsafemulti("calculating M08", self.logf, self.nps) + highZ = None + if self.logO35007O2 is not None: + self.mds['M08_O3O2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O3O2']] * self.nm).T + coefs[0] = coefs[0] - self.logO35007O2 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + #the two cumsum assure that if the condition for the ith element + #of indx is [False, False] then after the first cumsum(1) is [0,0] + #[False, True] is [0,1] + #[True, True] is [1,2] + #but (here is the kicker) [True, False] is [1,1]. + #Because i want only one solution + #(i'll settle for the first one occurring) [1,1] is ambiguous. + #The second cumsum(1) makes + #[0,0]->[0,0], [0,1]->[0,1], [1,2]->[1,3] and finally [1,1]->[1,2] + + self.mds['M08_O3O2'][(indx.sum(1)) > 0] = sols[indx].real + highZ = np.median(self.logO35007O2) < 0 + if self.logN2Ha is not None: + self.mds['M08_N2Ha'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['N2Ha']] * self.nm).T + coefs[0] = coefs[0] - self.logN2Ha + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_N2Ha'][(indx.sum(1)) > 0] = sols[indx].real + if highZ is None: + highZ = np.median(self.logN2Ha) > -1.3 + + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute M08_R23 without R23", self.logf, self.nps) + else: + self.mds['M08_R23'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['R23']] * self.nm).T + coefs[0] = coefs[0] - self.logR23 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real >= 8.0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_R23'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real <= 8.0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_R23'][(indx.sum(1)) > 0] = sols[indx].real + if not allM08: + return + else: + printsafemulti("calculating other M08s", self.logf, self.nps) + + if self.logO3Hb is not None: + + self.mds['M08_O3Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O3Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO3Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real >= 7.9)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O3Hb'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real <= 7.9)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O3Hb'][(indx.sum(1)) > 0] = sols[indx].real + + if self.logO2Hb is not None: + self.mds['M08_O2Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O2Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO2Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real >= 8.7)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real <= 8.7)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + + + + if self.hasO3 and self.hasN2: + self.mds['M08_O3N2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O3N2']] * self.nm).T + coefs[0] = coefs[0] - np.log(self.O35007 / self.N26584) * self.dustcorrect(k_O35007, k_N2) + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O3N2'][(indx.sum(1)) > 0] = sols[indx].real + + #@profile + def calcKD02_N2O2(self): + ## Kewley & Dopita (2002) estimates of abundance + ## KD02 + # KD02 [N2]/[O2] estimate (can be used for whole log(O/H)+12 range, + # but rms scatter increases to 0.11 rms for log(O/H)+12 < 8.6 + # rms = 0.04 for + # log(O/H)+12 > 8.6 + # uses equation (4) from KD02 paper + # FED: i vectorized the hell out of this function!!! + # from a 7 dimensional if/for loop to 1 if and 1 for :-D + #vectorizing makes fed happy ... + + printsafemulti("calculating KD02_N2O2", self.logf, self.nps) + + if self.hasN2 and self.hasO2 and self.hasHa and self.hasHb: + self.mds['KD02_N2O2'] = np.zeros(self.nm) + float('NaN') + if not self.hasN2O2: + printsafemulti("WARNING: must calculate logN2O2 first", self.logf, self.nps) + self.calcNIIOII() + if not self.hasN2O2 or self.N2O2_roots is None or sum(np.isnan(self.N2O2_roots.flatten())) == len(self.N2O2_roots.flatten()): + printsafemulti("WARNING: cannot calculate N2O2", self.logf, self.nps) + return -1 + roots = self.N2O2_roots.T + for k in range(4): + indx = (abs(roots[k]) >= 7.5) * (abs(roots[k]) <= 9.4) * (roots[k][:].imag == 0.0) + self.mds['KD02_N2O2'][indx] = abs(roots[k][indx]) + else: + printsafemulti("WARNING: need NII6584 and OII3727 and Ha and Hb to calculate this. did you run setO() setHab() and setNII()?", self.logf, self.nps) + return 1 + + #@profile + def calcKK04_N2Ha(self): + # calculating [N2]/Ha abundance estimates using [O3]/[O2] also + printsafemulti("calculating KK04_N2Ha", self.logf, self.nps) + + if self.mds['KD02_N2O2'] is None: + self.calcKD02_N2O2() + if self.mds['KD02_N2O2'] is None or sum(np.isnan(self.mds['KD02_N2O2'])) == self.nm: + printsafemulti("WARNING: without KD02_N2O2 cannot calculate KK04_N2Ha properly, but we will do our best...", self.logf, self.nps) + Z_new_N2Ha = np.zeros(self.nm) + 8.6 + else: + Z_new_N2Ha = self.mds['KD02_N2O2'].copy() # was 8.6 + + if self.hasN2 and self.hasHa: + logq_save = np.zeros(self.nm) + convergence, tol, ii = 100, 1.0e-3, 0 + if self.hasO3O2: + # calculating logq using the [N2]/[O2] + # metallicities for comparison + while convergence > tol and ii < 100: + ii += 1 + self.logq = self.calclogq(Z_new_N2Ha) + Z_new_N2Ha = nppoly.polyval(self.logN2Ha, [7.04, 5.28, 6.28, 2.37]) - \ + self.logq * nppoly.polyval(self.logN2Ha, [-2.44, -2.01, -0.325, +0.128]) + \ + 10 ** (self.logN2Ha - 0.2) * self.logq * (-3.16 + 4.65 * self.logN2Ha) + convergence = np.abs(self.logq - logq_save).mean() + logq_save = self.logq.copy() + if ii >= 100: + printsafemulti("WARNING: loop did not converge", self.logf, self.nps) + Z_new_N2Ha = np.zeros(self.nm) + float('NaN') + else: + self.logq = 7.37177 * np.ones(self.nm) + Z_new_N2Ha = nppoly.polyval(self.logN2Ha, [7.04, 5.28, 6.28, 2.37]) - \ + self.logq * nppoly.polyval(self.logN2Ha, [-2.44, -2.01, -0.325, +0.128]) + \ + 10 ** (self.logN2Ha - 0.2) * self.logq * (-3.16 + 4.65 * self.logN2Ha) + self.mds['KK04_N2Ha'] = Z_new_N2Ha + indx = self.logN2Ha > 0.8 + self.mds['KK04_N2Ha'][indx] = float('NaN') + else: + printsafemulti("WARNING: need NII6584 and Ha to calculate this. did you run setHab() and setNII()?", self.logf, self.nps) + + #@profile + def calcKK04_R23(self): + # Kobulnicky & Kewley 2004 + # calculating upper and lower metallicities for objects without + # Hb and for objects without O3 and/or O2 + + printsafemulti("calculating KK04_R23", self.logf, self.nps) + #this is in the original code but not used :( + #if self.hasN2 and self.hasHa: + #logq_lims=[6.9,8.38] + #logN2Ha=np.log10(self.N26584/self.Ha) CHECK!! why remove dust correction?? + #Z_new_N2Ha_lims= np.atleast_2d([1.0,1.0]).T*nppoly.polyval(self.logN2Ha,[7.04, 5.28,6.28,2.37])- + #np.atleast_2d( logq_lims).T*nppoly.polyval(self.logN2Ha,[-2.44,-2.01,-0.325,0.128])+ + #np.atleast_2d(logq_lims).T*(10**(self.logN2Ha-0.2)*(-3.16+4.65*self.logN2Ha)) + # R23 diagnostics from Kobulnicky & Kewley 2004 + + Zmax = np.zeros(self.nm) + # ionization parameter form logR23 + if not self.hasO3O2: + logq = np.zeros(self.nm) + else: + if self.Z_init_guess is None: + self.initialguess() + Z_new = self.Z_init_guess.copy() + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + else: + logqold, convergence, ii = np.zeros(self.nm) + 100, 100, 0 + tol = 1e-4 + #3 iterations are typically enought to achieve convergence KE08 A2.3 + while convergence > tol and ii < 100: + Zmax = Zmax * 0.0 + ii += 1 + logq = self.calclogq(Z_new) + Zmax[(logq >= 6.7) * (logq < 8.3)] = 8.4 + # maximum of R23 curve: + Z_new = nppoly.polyval(self.logR23, [9.72, -0.777, -0.951, -0.072, -0.811]) - \ + logq * nppoly.polyval(self.logR23, [0.0737, -0.0713, -0.141, 0.0373, -0.058]) + indx = self.Z_init_guess <= Zmax + Z_new[indx] = nppoly.polyval(self.logR23[indx], [9.40, 4.65, -3.17]) - \ + logq[indx] * nppoly.polyval(self.logR23[indx], [0.272, 0.547, -0.513]) + convergence = np.abs((logqold - logq).mean()) + logqold = logq.copy() + if ii >= 100: + printsafemulti("WARNING: loop did not converge", self.logf, self.nps) + Z_new = np.zeros(self.nm) + float('NaN') + Z_new_lims = [nppoly.polyval(self.logR23, [9.40, 4.65, -3.17]) - \ + logq * nppoly.polyval(self.logR23, [0.272, 0.547, -0.513]), + nppoly.polyval(self.logR23, [9.72, -0.777, -0.951, -0.072, -0.811]) - \ + logq * nppoly.polyval(self.logR23, [0.0737, -0.0713, -0.141, 0.0373, -0.058])] + Z_new[(Z_new_lims[0] > Z_new_lims[1])] = None + self.mds['KK04_R23'] = Z_new + + #@profile + def calcKDcombined(self): + # KD02comb Kewley, L. J., & Dopita, M. A., 2002, ApJ + # updated in KE08 + # ### KD02 [NII]/[OII] estimate ### + # (can be used for log(O/H)+12 > 8.6 only) + + printsafemulti("calculating KD_combined", self.logf, self.nps) + + #We first use the + #[N ii]/[O ii] ratio to determine whether it lies on the upper + #or lower R23 branch + + if self.mds['KD02_N2O2'] is None: + self.calcKD02_N2O2() + if self.mds['KK04_N2Ha'] is None: + self.calcKK04_N2Ha() + if self.logR23 is None: + self.calcR23() + if self.mds['M91'] is None: + printsafemulti("WARNING: Must first calculate M91", self.logf, self.nps) + self.calcM91() +# if self.mds['Z94'] is None: +# printsafemulti( "WARNING: Must first calculate Z94",self.logf,self.nps) +# self.calcZ94() + if self.mds['KK04_R23'] is None: + printsafemulti("WARNING: Must first calculate KK04_R23", self.logf, self.nps) + self.calcKK04_R23() + if not self.hasHa and not self.hasHb: + printsafemulti("WARNING: need Ha and Hb for this. did you run setHab()?", self.logf, self.nps) + + #alternative way to calculate KD02_N2O2, stated in the paper KD02, + #valid in high Z regimes (Z>8.4) + #but we forego it + #if not self.logN2O2 is None: + # self.mds['KD02_N2O2']=np.log10(8.511e-4*(1.54020+1.26602*self.logN2O2+0.167977*self.logN2O2**2))+12. + #else: self.mds['KD02_N2O2']=np.zeros(self.nm)+float('NaN') + + # ionization parameter + # calculate an initial ionization parameter by assuming + # a nominal lower branch [12 + log (O/H ) = 8.2] + # or upper branch [12 + log (O/H ) = 8.7] metallicity using + # equation (13) from KK04 + logq = np.zeros(self.nm) + if self.hasN2 and self.hasO2 and self.hasHb and self.hasHa and self.hasO3O2: + logq = self.calclogq(self.mds['KD02_N2O2']) + logq[self.mds['KD02_N2O2'] >= 8.4] = self.logq[self.mds['KD02_N2O2'] >= 8.4] + else: + if self.Z_init_guess is None: + self.initialguess() + logq = self.calclogq(self.Z_init_guess) + #FED: CHECK: the paragraph below makes sense in words but i dont see whereit ie enforced. + # if log([NII]/[OII]) after extinction correction is <-1.5, then check the data. + # if it is only slightly less than 1.5, then this can be a result of either noisy + # data, inaccurate fluxes or extinction correction, or a higher ionization parameter + # than modelled. + # For these cases, the average of the M91,Z94 and C01 should be used. + + # KD02 R23 estimate (not reliable for 8.4 < log(O/H)+12 < 8.8) + # uses [NII]/[OII] estimate as initial guess - this can be changed below + + self.mds['KD02comb'] = np.zeros(self.nm) + float('NaN') + + indx_ig = self.Z_init_guess > 8.4 + if self.mds['KD02_N2O2'] is not None: + self.mds['KD02comb'][indx_ig] = self.mds['KD02_N2O2'][indx_ig].copy() + if self.mds['KK04_N2Ha'] is not None: + self.mds['KD02comb'][~indx_ig] = self.mds['KK04_N2Ha'][~indx_ig].copy() + if self.mds['KK04_R23'] is not None and self.mds['M91'] is not None: + # if [NII]/[OII] abundance available + # and [NII]/Ha abundance < 8.4, then use R23. + indx = (~np.isnan(self.mds['KK04_R23'])) * (~np.isnan(self.mds['M91'])) * (~indx_ig) + self.mds['KD02comb'][indx] = 0.5 * (self.mds['KK04_R23'][indx].copy() + self.mds['M91'][indx].copy()) + + else: + printsafemulti("WARNING: cannot calculate KK04comb because KK04_R23 or M91, failed", self.logf, self.nps) + +#######################these are the metallicity diagnostics################## +#@profile + def calcPM14(self): + # Perez-Montero 2014 + # (can be used for for log(O/H)+12 > 8.6 only) + import os + from subprocess import Popen, PIPE, STDOUT + from StringIO import StringIO + + printsafemulti("calculating HIICHI", self.logf, self.nps) + fin_hii_chi = open(os.getenv('HIICHI_DIR') + '/in.tmp', 'w') + + if not self.hasHb: + printsafemulti("cannot calculate HIICHI without Hbeta", self.logf, self.nps) + return -1 + + ratios = np.zeros((5, self.nm)) + + if self.R2 is not None: + ratios[0] = self.R2 + elif self.hasO2: + ratios[0] = ((self.O23727 / self.Hb) * self.dustcorrect(k_O2, k_Hb, flux=True)) + else: + ratios[0] = np.array(['0 '] * self.nm) + + #we will never have 4363... + ratios[1] = np.zeros(self.nm) + + if self.hasO3Hb: + ratios[2] = self.O3Hb + elif self.hasO3: + ratios[2] = ((self.O35007 / self.Hb) + self.dustcorrect(k_O35007, k_Hb, flux=True)) # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + else: + ratios[2] = np.zeros(self.nm) + + if self.hasN2: + ratios[3] = self.N26584 / self.Hb + else: + ratios[3] = np.zeros(self.nm) + + if self.hasS2Hb: + ratios[4] = self.S2Hb + elif self.hasS2 and self.hasS26731: + ratios[4] = (((self.S26717 + self.S26731) / self.Hb) + self.dustcorrect(k_S2, k_Hb, flux=True)) + else: + ratios[4] = np.zeros(self.nm) + + + for ni in range(self.nm): + fin_hii_chi.write('%f %f %f %f %f\n' % (ratios[0][ni], ratios[1][ni], ratios[2][ni], ratios[3][ni], ratios[4][ni])) + fin_hii_chi.close() + os.system("ln -s %s/C13*dat . " % os.getenv('HIICHI_DIR')) + print "\n\n\n\n\n" + #os.system("python %s/HII-CHI-mistry_v01.2.py in.tmp"%os.getenv('HIICHI_DIR')) + #os.system("python %s/HII-CHI-mistry_v01.2.py %s/in.tmp"%(os.getenv('HIICHI_DIR'),os.getenv('HIICHI_DIR'))) + p = Popen(['python', '%s/HII-CHI-mistry_v01.2.py' % os.getenv('HIICHI_DIR'), '%s/in.tmp' % os.getenv('HIICHI_DIR')], stdout=PIPE, stdin=PIPE, stderr=STDOUT) + out, err = p.communicate(input='%s/in.tmp' % os.getenv('HIICHI_DIR')) + print "\n\n\n\n\n" + out = StringIO(out) + # for l in enumerate(out): + # if l[0].isdigit(): + # break + out = out.readlines()[12:] + self.mds['PM14'] = np.zeros((self.nm)) + self.mds['PM14err'] = np.zeros((self.nm)) + for i, l in enumerate(out): + self.mds['PM14'][i], self.mds['PM14err'][i] = map(float, l.split()[3:5]) + #data = np.loadtxt(out, skiprows=12, usecols=(3,4))#, dtype=[('lOH','f'),('elOH','f')], delimiter=",", unpack = True) + print self.mds + os.system('rm -r C13*dat') diff --git a/build/scripts-2.7/pylabsetup.py b/build/scripts-2.7/pylabsetup.py new file mode 100755 index 0000000..5ffef05 --- /dev/null +++ b/build/scripts-2.7/pylabsetup.py @@ -0,0 +1,41 @@ +import matplotlib as mpl +import pylab as plt + +mpl.rcParams.update(mpl.rcParamsDefault) +mpl.rcParams['font.size'] = 26. +mpl.rcParams['font.family'] = 'serif' +#mpl.rcParams['font.family'] = 'serif' +mpl.rcParams['font.serif'] = ['Times New Roman', 'Times', 'Palatino', 'Charter', 'serif'] +mpl.rcParams['font.sans-serif'] = ['Helvetica'] +mpl.rcParams['axes.labelsize'] = 24 +mpl.rcParams['xtick.labelsize'] = 22. +mpl.rcParams['ytick.labelsize'] = 22. +mpl.rcParams['xtick.major.size'] = 15. +mpl.rcParams['xtick.minor.size'] = 10. +mpl.rcParams['ytick.major.size'] = 15. +mpl.rcParams['ytick.minor.size'] = 10. +#mpl.rcParams['figure.autolayout']= True + +#fontsize=26 +#mpl.rc('axes', titlesize=fontsize) +#mpl.rc('axes', labelsize=fontsize) +#mpl.rc('xtick', labelsize=fontsize) +#mpl.rc('ytick', labelsize=fontsize) +#mpl.rc('font', size=fontsize, family='serif', serif='Utopia', +# style='normal', variant='normal', +# stretch='normal', weight='normal') +#mpl.rc('font',**{'family':'serif','serif':[ 'Times New Roman', 'Times', 'serif'], +# 'sans-serif':['Helvetica'], 'size':19, +# 'weight':'normal'}) +mpl.rc('axes', **{'linewidth' : 1.2}) +mpl.rc('axes', **{'labelweight': 'normal', 'linewidth': 1}) +mpl.rc('axes', **{'labelweight': 'normal', 'linewidth': 1}) +mpl.rc('ytick', **{'major.pad': 5, 'color': 'k'}) +mpl.rc('xtick', **{'major.pad': 5, 'color': 'k'}) +params = {'legend.fontsize': 24, + 'legend.numpoints': 1, + 'legend.handletextpad': 1 + } + +plt.rcParams.update(params) +plt.minorticks_on() diff --git a/build/scripts-3.6/mcz.py b/build/scripts-3.6/mcz.py new file mode 100755 index 0000000..f429d37 --- /dev/null +++ b/build/scripts-3.6/mcz.py @@ -0,0 +1,791 @@ +#!/Users/howk/miniconda3/bin/python +from __future__ import print_function +import os +import sys +import argparse +import warnings + +import numpy as np +import scipy.stats as stats +from scipy.special import gammaln +from scipy import optimize +import matplotlib.pyplot as plt +from matplotlib.ticker import FormatStrFormatter +import csv as csv + +#modules of this package +import pylabsetup + +#import metallicity_save2 as metallicity +import metallicity as metallicity + +import itertools +import multiprocessing as mpc + +###versin 1.3 February 2016 +#fixed compatyibiity issue with numphy >1.9 in the val to np.histogram https://github.com/numpy/numpy/issues/6469 + +###version 1.3.1 April 2016 +#works with version 0.7 of pyqz (current version) +#previous versions only worked with version 0.5. +#a check for the version of pyqz is implememted and a different call is issued for version >0.5 and <=0.5 + + + +# Define the version of +__version__ = '1.3.1' + +NM0 = 0 # setting this to say N>0 starts the calculation at measurement N. +#this is only for exploratory purposes as the code bugs out before +#plotting and printing the results + +PROFILING = True +PROFILING = False + +alllines = ['[OII]3727', 'Hg','Hb', '[OIII]4959', '[OIII]5007', '[OI]6300', 'Ha', '[NII]6584', '[SII]6717', '[SII]6731', '[SIII]9069', '[SIII]9532'] +morelines = ['E(B-V)', 'dE(B-V)', 'scale_blue', 'd scale_blue'] + +MAXPROCESSES = 10 + +#pickle may not be installed +NOPICKLE = False +try: + import pickle +except ImportError: + NOPICKLE = True + +CLOBBER = False +VERBOSE = False +UNPICKLE = False +ASCIIOUTPUT = False +ASCIIDISTRIB = False +RUNSIM = True +NOPLOT = False +BINMODE = 'k' +binning = {'bb': 'Bayesian blocks', 'k': "Knuth's rule", + 'd': "Doane's formula", 's': r'$\sqrt{N}$', + 't': r'$2 N^{1/3}$', 'kd': 'Kernel Density'} + +MP = False + + +def is_number(s): + if not type(s) is np.string_: + try: + float(s) + return True + except ValueError: + return False + return False + + +def smart_open(logfile=None): + if logfile and logfile != '-': + fh = open(logfile, 'w') + else: + fh = sys.stdout + return fh + + +def getknuth(m, data, N): + m = int(m) + if m > N: + return [-1] + bins = np.linspace(min(data), max(data), int(m) + 1) + try: + nk, bins = np.histogram(data, bins) + return -(N * np.log(m) + gammaln(0.5 * m) - m * gammaln(0.5) - gammaln(N + 0.5 * m) + np.sum(gammaln(nk + 0.5))) + except: + return [-1] + + +def knuthn(data, maxM=None): + assert data.ndim == 1, "data must be 1D array to calculate Knuth's number of bins" + N = data.size + if not maxM: + maxM = 5 * np.sqrt(N) + m0 = 2.0 * N ** (1. / 3.) + gk = getknuth + if gk == [-1]: + return m0, 't' + mkall = optimize.fmin(gk, m0, args=(data, N), disp=VERBOSE, maxiter=30) # , maxfun=1000)#[0] + mk = mkall[0] + if mk > maxM or mk < 0.3 * np.sqrt(N): + mk = m0 + return mk, 't' + return mk, 0 + + +############################################################################## +##The input data +############################################################################## +##Reads the flux file and returns it as an array. +##Ignores non-numeric lines +##Returns (flux array,num) +############################################################################## +def readfile(filename): + noheader = 1 + findex = -1 + f = open(filename, 'r') + l0 = f.readline().replace(' ', '') + l1 = f.readline().split() + if l0.startswith('#') or l0.startswith(';'): + # Modified to account for non-standard header (\t instead of , separated) jch + temp_header = l0.strip().replace(";", '').replace("#", '') + if temp_header.find(',') == -1: + header = temp_header.split('\t') + else: + header = temp_header.split(',') + header[0] = header[0].replace(' ', '') + header = header[:len(l1)] + else: + noheader = 0 + header = ['galnum'] + alllines + ['flag'] + morelines + header = header[:len(l1)] + + usecols = [] + newheader = [] + for i,h in enumerate(header): + if h in alllines + ['galnum']: + usecols.append(i) + newheader.append(h) + + header = newheader + + formats = ['S10'] + ['f'] * (len(header) - 1) + + if 'flag' in header: + findex = header.index('flag') + formats[findex] = 'S10' + + bstruct = {} + for i, k in enumerate(header): + bstruct[k] = [i, 0] + b = np.loadtxt(filename, skiprows=noheader, dtype={'names': header, 'formats': formats}, comments=';',usecols=usecols) + if b.size == 1: + b = np.atleast_1d(b) + for i, k in enumerate(header): + if not k == 'flag' and is_number(b[k][0]): + bstruct[k][1] = np.count_nonzero(b[k]) + sum(np.isnan(b[k])) + j = len(b['galnum']) + return b, j, bstruct + + +def ingest_data(filename, path): + ###Initialize### + measfile = os.path.join(path, filename + "_meas.txt") + errfile = os.path.join(path, filename + "_err.txt") + + ###read the max, meas, min flux files### + meas, nm, bsmeas = readfile(measfile) + err, nm, bserr = readfile(errfile) + try: + snr = (meas[: + ,1:].view(np.float32).reshape(meas[: + ,1:].shape + (-1, ))[: + ,1:]) / (err[: + ,1:].view(np.float32).reshape(err[: + ,1:].shape + (-1, ))[: + ,1:]) + if snr[~np.isnan(snr)].any() < 3: + raw_input('''WARNING: signal to noise ratio smaller than 3 + for at least some lines! You should only use SNR>3 + measurements (return to proceed)''') + except (IndexError, TypeError): + pass + return (filename, meas, err, nm, path, (bsmeas, bserr)) + +def input_data(filename, path): + p = os.path.join(path, "input") + assert os.path.isdir(p), "bad data directory %s" % p + if os.path.isfile(os.path.join(p, filename + '_err.txt')): + if os.path.isfile(os.path.join(p, filename + '_meas.txt')): + return ingest_data(filename, path=p) + print ("Unable to find _meas and _err files ", filename + + '_meas.txt', filename + '_err.txt', "in directory ", p) + return -1 + +############################################################################## +##returns a random distribution. In the deployed version of the code this is a gaussian distribution, but the user can include her or his distribution. +##return a random sample of size n +############################################################################## +def errordistrib(distrargs, n, distype='normal'): + if distype == 'normal': + try: + mu, sigma = distrargs + except: + print ("for normal distribution distargs must be a 2 element tuple") + return -1 + return np.random.normal(mu, sigma, n) + else: + print ("distribution not supported") + return -1 + + +############################################################################## +##returns appropriate bin size for the number of data +##mode 'k' calculates this based on Knuth's rule +##mode 'd' calculates this based on Doane's formula +##mode 's' calculates this based on sqrt of number of data +##mode 't' calculates this based on 2*n**1/3 (default) +############################################################################## +def getbinsize(n, data, ): + if BINMODE == 'd': + g1 = np.abs(stats.mstats.moment(data, moment=3)) # /data.std()) + s1 = np.sqrt(float(n) / 6.0) + #s1=1.0/np.sqrt(6.*(n-2.)/((n+1.)*(n+3.))) + k = 1 + np.log2(n) + np.log2(1 + (g1 * s1)), 0 + elif BINMODE == 's': + k = np.sqrt(n), 0 + elif BINMODE == 't': + k = 2. * n ** (1. / 3.), 0 + else: + k = knuthn(data) + return k + + +############################################################################## +##Check if hist files already exist and need to be replaced +############################################################################## +def checkhist(snname, Zs, nsample, i, path): + global CLOBBER + + name = '%s_n%d_%s_%d' % ((snname, nsample, Zs, i + 1)) + outdir = os.path.join(path, 'hist') + outfile = os.path.join(outdir, name + ".pdf") + if os.path.isfile(outfile) and not CLOBBER: + replace = raw_input("replacing existing image files, starting with: %s ? [Y/n]\n" % outfile).lower() + assert(not (replace.startswith('n'))), "save your existing output directory under another name first" + CLOBBER = True + + +############################################################################## +##Save the result as histogram as name +############################################################################## +#@profile +def savehist(data, snname, Zs, nsample, i, path, nmeas, measnames, verbose=False, fs=24, reserr=None): + global BINMODE + #global NOPLOT + + name = '%s_n%d_%s_%d' % ((snname, nsample, Zs, i + 1)) + outdir = os.path.join(path, 'hist') + outfile = os.path.join(outdir, name + ".pdf") + if not NOPLOT: + fig = plt.figure(figsize=(11, 8)) + plt.clf() + + ####kill outliers, infinities, and bad distributions### + data = data[np.isfinite(data)] + + n = data.shape[0] + kde = None + if not n > 0: + if verbose: + print ("data must be an actual distribution (n>0 elements!, %s)" % Zs) + return "-1,-1,_1", [], kde + + if data.shape[0] <= 0 or np.sum(data) <= 0: + print ('{0:15} {1:20} {2:>13d} {3:>7d} {4:>7d} '\ + .format(snname, Zs, -1, -1, -1)) + return "-1, -1, -1", [], kde + try: + ###find C.I.### + median, pc16, pc84 = np.percentile(data, [50, 16, 84]) + std = np.std(data) + left = pc16 + right = pc84 + maxleft = median - std * 5 + maxright = median + std * 5 + if "%2f" % maxright == "%2f" % maxleft: + maxleft = median - 1 + maxright = median + 1 + if round(right, 6) == round(left, 6) and round(left, 6) == round(median, 6): + print ('{0:15} {1:20} {2:>13.3f} -{3:>7.3f} +{4:>7.3f} (no distribution)'.format(snname, Zs, median, 0, 0)) + if reserr: + print ('+/- {0:.3f}'.format(reserr)) + return "%f\t %f\t %f" % (round(median, 3), round(median - left, 3), round(right - median, 3)), data, kde # "-1,-1,-1",[] + ###print out the confidence interval### + print ('{0:15} {1:20} {2:>13.3f} -{3:>7.3f} +{4:>7.3f}'.format(snname, + Zs, round(median, 3), + round(median - left, 3), + round(right - median, 3))) + if reserr: + print ('+/- {0:.3f}'.format(reserr)) + alpha = 1.0 + + ######histogram###### + if BINMODE == 'kd': + ##if sklearn is available use it to get Kernel Density + try: + from sklearn.neighbors import KernelDensity + except ImportError: + print ('''sklearn is not available, + thus we cannot compute kernel density. + switching to bayesian blocks''') + BINMODE = 'bb' + if BINMODE == 'kd': + ##bw is chosen according to Silverman 1986 + bw = 1.06 * std * n ** (-0.2) + numbin, bm = getbinsize(data.shape[0], data) + distrib = np.histogram(data, bins=int(numbin), density=True) + ###make hist### + counts, bins = distrib[0], distrib[1] + widths = np.diff(bins) + countsnorm = counts / np.max(counts) + + if bw > 0: + kde = KernelDensity(kernel='gaussian', bandwidth=bw).fit(data[: + , np.newaxis]) + kdebins = np.linspace(maxleft, maxright, 1000)[: + , np.newaxis] + log_dens = kde.score_samples(kdebins) + dens = np.exp(log_dens) + norm = countsnorm.sum() * (bins[1] - bins[0]) / dens.sum() / (kdebins[1] - kdebins[0]) + if not NOPLOT: + plt.fill(kdebins[: + ,0], dens * norm, fc='#7570b3', alpha=0.8) + alpha = 0.5 + + ###find appropriate bin size### + else: + if BINMODE == 'bb': + ##if astroML is available use it to get Bayesian blocks + bm = 0 + try: + from astroML.plotting import hist as amlhist + if BINMODE == 'bb': + distrib = amlhist(data, bins='blocks', normed=True) + if not NOPLOT: + plt.clf() + except ImportError: + print ("bayesian blocks for histogram requires astroML to be installed") + print ("defaulting to Knuth's rule ") + ##otherwise + numbin, bm = getbinsize(data.shape[0], data) + distrib = np.histogram(data, bins=int(numbin), density=True) + else: + numbin, bm = getbinsize(data.shape[0], data) + distrib = np.histogram(data, bins=int(numbin), density=True) + + ###make hist### + counts, bins = distrib[0], distrib[1] + widths = np.diff(bins) + countsnorm = counts / np.max(counts) + + ###plot hist### + if NOPLOT: + return "%f\t %f\t %f" % (round(median, 3), round(median - left, 3), round(right - median, 3)), data, kde + + plt.bar(bins[:-1], countsnorm, widths, color=['gray'], alpha=alpha) + plt.minorticks_on() + plt.gca().xaxis.set_major_formatter(FormatStrFormatter('%.2f')) + plt.xlim(maxleft, maxright) + + #the following lines assure the x tick label is + #within the length of the x axis + xticks = plt.xticks()[0] + dx = xticks[-1] - xticks[-2] + xticks = xticks[(xticks < maxright) * (xticks > maxleft)] + if (maxright - xticks[-1]) < 0.25 * dx: + maxright = maxright + 0.25 * dx + maxleft = maxleft - 0.25 * dx + plt.xlim(maxleft, maxright) + plt.xticks(xticks, ['%.2f' % s for s in xticks]) + + plt.ylim(0, 1.15) + plt.yticks(np.arange(0.2, 1.3, 0.2), ["%.1f" % x for x in np.arange(0.2, 1.1, 0.2)]) + plt.axvspan(left, right, color='DarkOrange', alpha=0.4) + plt.axvline(x=median, linewidth=2, color='white', ls='--') + + #labels and legends + st = '%s ' % (snname) + plt.annotate(st, xy=(0.13, 0.6), xycoords='axes fraction', size=fs, fontweight='bold') + st = '%s ' % (Zs.replace('_', ' ')) + plt.annotate(st, xy=(0.61, 0.93), xycoords='axes fraction', fontsize=fs, fontweight='bold') + st = 'measurement %d of %d\n %s\nmedian: %.3f\n16th Percentile: %.3f\n84th Percentile: %.3f' \ + % (i + 1, nmeas, measnames[i], round(median, 3), round(left, 3), round(right, 3)) + plt.annotate(st, xy=(0.61, 0.65), xycoords='axes fraction', fontsize=fs) + effectiven = len(data[~np.isnan(data)]) + if effectiven: + st = 'MC sample size %d (%d)\nhistogram rule: %s' % (effectiven, nsample, binning[BINMODE]) + if bm: + if effectiven < nsample: + st = 'MC sample size %d (%d)\nhistogram rule: %s' % (effectiven, nsample, binning[bm]) + else: + st = 'MC sample size %d\nhistogram rule: %s' % (nsample, binning[bm]) + plt.annotate(st, xy=(0.61, 0.55), xycoords='axes fraction', fontsize=fs - 5) + if "E(B-V)" in Zs: + plt.xlabel('E(B-V) [mag]') + outfile = outfile.replace('(', '').replace(')', '') + elif "logR23" in Zs: + plt.xlabel('logR23') + else: + plt.xlabel('12+log(O/H)') + plt.ylabel('relative counts') + plt.savefig(outfile, format='pdf') + plt.close(fig) + + return "%f\t %f\t %f" % (round(median, 3), round(median - left, 3), round(right - median, 3)), data, kde + + except (OverflowError, AttributeError, ValueError): + if VERBOSE: + print (data) + print (name, 'had infinities (or something in plotting went wrong)') + return "-1, -1,-1", [], None + + +def calc((i, (sample, flux, err, nm, bss, mds, disp, dust_corr,dust_blue, + verbose, res, scales, nps, logf))): + logf = sys.stdout + logf.write("\n\nreading in measurements %d\n"%(i + 1)) + fluxi = {} # np.zeros((len(bss[0]),nm),float) + for k in bss[0].iterkeys(): + logf.write('{0:15} '.format(k)) + logf.write('{0:0.2} +/- {1:0.2}\n'.format(flux[k][i], err[k][i])) + #print >> logf, '{0:15} '.format(k), + #print >> logf, '{0:0.2} +/- {1:0.2}'.format(flux[k][i], err[k][i]) + fluxi[k] = flux[k][i] * np.ones(len(sample[i])) + err[k][i] * sample[i] + warnings.filterwarnings("ignore") + success = metallicity.calculation(scales[i], fluxi, nm, mds, nps, logf, + disp=disp, verbose=verbose, + dust_corr=dust_corr,dust_blue=dust_blue) + if success == -1: + logf.write("MINIMUM REQUIRED LINES: '[OII]3727','[OIII]5007','[NII]6584','[SII]6717'\n") + #print >> logf, "MINIMUM REQUIRED LINES: '[OII]3727','[OIII]5007','[NII]6584','[SII]6717'" + + for key in scales[i].mds.iterkeys(): + if key in res.keys(): + res[key][i] = scales[i].mds[key] + if res[key][i] is None: + res[key][i] = [float('NaN')] * len(sample) + return res + + +############################################################################## +## The main function. takes the flux and its error as input. +## filename - a string 'filename' common to the three flux files +## flux - np array of the fluxes +## err - the flux errors, must be the same dimension as flux +## nsample - the number of samples the code will generate. Default is 100 +## errmode - determines which method to choose the bin size. +## mode 'k' calculates this based on Knuth's rule (default) +## mode 'd' calculates this based on Doane's formula +## mode 's' calculates this based on sqrt of number of data +## mode 't' calculates this based on 2*n**1/3 +############################################################################## +#@profile +def run((name, flux, err, nm, path, bss), nsample, mds, multiproc, logf, + unpickle=False, dust_corr=True, dust_blue=False, verbose=False, fs=24): + global RUNSIM # ,BINMODE#,NOPLOT + assert(len(flux[0]) == len(err[0])), "flux and err must be same dimensions" + assert(len(flux['galnum']) == nm), "flux and err must be of declaired size" + assert(len(err['galnum']) == nm), "flux and err must be same dimensions" + + #increasing sample by 10% to assure robustness against rejected samples + newnsample = nsample + if nsample > 1: + newnsample = int(nsample + 0.1 * nsample) + + p = os.path.join(path, '..') + + ###retrieve the metallicity keys + Zs = metallicity.get_keys() + Zserr = metallicity.get_errkeys() + + ###make necessary paths for output files + if not os.path.exists(os.path.join(p, 'output', '%s' % name)): + os.makedirs(os.path.join(p, 'output', '%s' % name)) + if not os.path.exists(os.path.join(p, 'output', '%s' % name, 'hist')): + os.makedirs(os.path.join(p, 'output', '%s' % name, 'hist')) + binp = os.path.join(p, 'output', '%s' % name) + picklefile = os.path.join(binp, '%s_n%d.pkl' % (name, nsample)) + if VERBOSE: + print ("output files will be stored in ", binp) + if not CLOBBER and not NOPLOT: + for key in Zs: + for i in range(NM0, nm): + checkhist(name, key, nsample, i, binp) + if unpickle: + RUNSIM = False + if not os.path.isfile(picklefile): + raw_input("missing pickled file for this simulation: name, nsample.\nrun the MonteCarlo? Ctr-C to exit, Return to continue?\n") + RUNSIM = True + else: + pklfile = open(picklefile, 'rb') + res = pickle.load(pklfile) + + if RUNSIM: + ###Sample 'nsample' points from a gaussian centered on 0 with std 1 + mu = 0 + sigma = 1 + dargs = (mu, sigma) + if nsample == 1: + sample = [np.array([mu]) for i in range(NM0, nm)] + else: + sample = [errordistrib(dargs, newnsample) for i in range(NM0, nm)] + if sample == -1: + return -1 + + ###Start calculation### + ## the flux to be feed to the calculation will be + ## flux + error*i + ## where i is the sampled gaussian + if VERBOSE: + print ("Starting iteration") + + #initialize the dictionary + res = {} + reserr = {} + for key in Zs: + res[key] = [[] for i in range(NM0, nm)] + for key in Zserr: + reserr[key] = [[] for i in range(NM0, nm)] + #use only valid inputs + delkeys = [] + for k in bss[0].iterkeys(): + if k == 'flag' or k == 'galnum' or bss[0][k][1] == float('nan'): # or bss[1][k][1]==bss[0][k][1]: + delkeys.append(k) + for k in delkeys: + del bss[0][k] + del bss[1][k] + + import metscales as ms + nps = min(mpc.cpu_count() - 1 or 1, MAXPROCESSES) + + if multiproc and nps > 1: + scales = [ms.diagnostics(newnsample, logf, nps) for i in range(nm)] + + logf.write( "\n\n\nrunning on %d threads\n\n\n" % nps) + #print >> logf, "\n\n\nrunning on %d threads\n\n\n" % nps + second_args = [sample, flux, err, nm, bss, mds, VERBOSE, dust_corr, dust_blue, VERBOSE, res, scales, nps, logf] + pool = mpc.Pool(processes=nps) # depends on available cores + rr = pool.map(calc, itertools.izip(range(NM0, nm), itertools.repeat(second_args))) # for i in range(nm): result[i] = f(i, second_args) + + + for ri, r in enumerate(rr): + for kk in r.iterkeys(): + res[kk][ri] = r[kk][ri] + + pool.close() # not optimal! but easy + pool.join() + for key in scales[i].mds.iterkeys(): + if key in Zs: + res[key] = np.array(res[key]).T + elif key in reserr.keys(): + reserr[key] = np.array(reserr[key]).T + if res[key][i] is None: + res[key][i] = [float('NaN')] * len(sample) + + if VERBOSE: + print ("Iteration Complete") + else: + #looping over nm spectra + for i in range(NM0, nm): + scales = ms.diagnostics(newnsample, logf, nps) + logf.write("\n\n measurements %d\n"%(i + 1)) + #print >> logf, "\n\n measurements ", i + 1 + fluxi = {} + for k in bss[0].iterkeys(): + #shuffling the distribution each time + np.random.shuffle(sample[i]) + + logf.write('{0:15} '.format(k)) + logf.write('{0:0.2} +/- {1:0.2}\n'.format(flux[k][i], + err[k][i])) + #print >> logf, '{0:15} '.format(k), + #print >> logf, '{0:0.2} +/- {1:0.2}'.format(flux[k][i], err[k][i]) + fluxi[k] = flux[k][i] * np.ones(len(sample[i])) + err[k][i] * sample[i] + warnings.filterwarnings("ignore") + logf.write("\n") + #print >> logf, "" + + success = metallicity.calculation(scales, fluxi, nm, mds, 1, logf, + disp=VERBOSE, verbose=VERBOSE, + dust_corr=dust_corr, dust_blue=dust_blue) + if success == -1: + print ("MINIMUM REQUIRED LINES: [OII]3727 & [OIII]5007, or [NII]6584, and Ha & Hb if you want dereddening") + #continue + + + for key in scales.mds.iterkeys(): + if key not in Zs: + continue + res[key][i] = scales.mds[key] + if res[key][i] is None: + res[key][i] = [float('NaN')] * newnsample + elif len(res[key][i]) < newnsample: + res[key][i] = res[key][i] + [float('NaN')] * (newnsample - len(res[key][i])) + + for key in scales.mds.iterkeys(): + if key in Zs: + res[key] = np.array(res[key]).T + if VERBOSE: + print ("Iteration Complete") + + #"WE CAN PICKLE THIS!" + #pickle this realization + if not NOPICKLE: + pickle.dump(res, open(picklefile, 'wb')) + + from matplotlib.font_manager import findfont, FontProperties + + if 'Time' not in findfont(FontProperties()): + fs = 20 + if VERBOSE: + print ("FONT: %s, %d" % (findfont(FontProperties()), fs)) + + ###Bin the results and save### + print ("\n\n") + print ('{0:15} {1:20} {2:>13} -{3:>5} +{4:>5} {5:11} {6:>7}'\ + .format("SN", "diagnostic", "metallicity", "34%", "34%", + "(sample size:", '%d)' % nsample)) + for i in range(NM0, nm): + if ASCIIOUTPUT: + fi = open(os.path.join(binp, '%s_n%d_%d.txt' % (name, nsample, i + 1)), 'w') + fi.write("%s\t Median Oxygen abundance (12+log(O/H))\t 16th percentile\t 84th percentile\n" % name) + + boxlabels = [] + datas = [] + print ("\n\nmeasurement ", + "%d/%d : %s-------------------------------------------------------------" % (i + 1, nm, flux[i]['galnum'])) + for key in Zs: + if nsample == -1: + try: + if ~np.isnan(res[key][i][0]): + print ('{0:15} {1:20} {2:>13.3f} -{3:>7.3f} +{4:>7.3f} (no distribution)'.format(name + ' %d' % (i + 1), key, res[key][i][0], 0, 0)) + except IndexError: + pass + else: + reserr = None + try: + if sum(~np.isnan(res[key][: + ,i])) > 0: + if ASCIIDISTRIB: + with open(os.path.join(binp, '%s_n%d_%s_%d.csv' % (name, nsample, key, i + 1)), "wb") as fidist: + writer = csv.writer(fidist) + writer.writerow(res[key][: + ,i]) + if 'PM14' in key: + print (reserr['PM14err']) + reserr = np.sqrt(~np.nansum(reserr['PM14err'][: + ,i] ** 2)) + sh, data, kde = savehist(res[key][: + ,i], name, key, nsample, i, binp, nm, flux[:]['galnum'], verbose=verbose, fs=fs, reserr=reserr) + s = key + "\t " + sh + '\n' + if ASCIIOUTPUT: + fi.write(s) + if key not in ["E(B-V)", "logR23"]: + boxlabels.append(key.replace('_', ' ')) + datas.append(data) + if BINMODE == 'kd' and not NOPICKLE: + pickleKDEfile = os.path.join(binp + '/%s_n%d_%s_%d_KDE.pkl' % (name, nsample, key, i + 1)) + if VERBOSE: + print ("KDE files will be stored in ", pickleKDEfile) + pickle.dump(kde, open(pickleKDEfile, 'wb')) + except (IndexError, TypeError): + pass + #make box_and_whiskers plot + fig = plt.figure(figsize=(8, 15)) + fig.subplots_adjust(bottom=0.18, left=0.18) + ax = fig.add_subplot(111) + plt.grid() + if len(datas) == 0: + continue + + bp = ax.boxplot(datas, patch_artist=True) + for box in bp['boxes']: + box.set(color='#7570b3', linewidth=2) + box.set(facecolor='DarkOrange', alpha=0.4) + for whisker in bp['whiskers']: + whisker.set(color='#7570b3', linewidth=2) + for cap in bp['caps']: + cap.set(color='#7570b3', linewidth=2) + for median in bp['medians']: + median.set(color='k', linewidth=2) + for flier in bp['fliers']: + flier.set(marker='o', color='#7570b3', alpha=0.4, mfc='#7570b3') + plt.title("measurement %d: %s" % (i + 1, flux[i]['galnum'])) + plt.xticks(range(1, len(boxlabels) + 1), boxlabels, rotation=90, fontsize=fs - 5) + plt.fill_between(range(1, len(boxlabels) + 1), [8.76] * len(boxlabels), [8.69] * len(boxlabels), facecolor='black', alpha=0.3) + plt.text(1.2, 8.705, "Solar Oxygen Abundance", alpha=0.7) + plt.gca().yaxis.set_major_formatter(FormatStrFormatter('%.2f')) + plt.ylabel('12+log(O/H)', fontsize=fs) + plt.savefig(binp + "/" + name + "_boxplot_n%d_%d.pdf" % (nsample, i + 1), format='pdf') + if ASCIIOUTPUT: + fi.close() + if VERBOSE: + print ("uncertainty calculation complete") + + #del datas + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('name', metavar='', type=str, help="the SN file name (root of the _min,_max file names") + parser.add_argument('nsample', metavar='N', type=int, help="number of iterations, minimum 100 (or 1 for no MC sampling)") + parser.add_argument('--clobber', default=False, action='store_true', help="replace existing output") + parser.add_argument('--binmode', default='k', type=str, choices=['d', 's', 'k', 't', 'bb', 'kd'], help='''method to determine bin size + {d: Duanes formula, s: n^1/2, t: 2*n**1/3(default), k: Knuth's rule, + bb: Bayesian blocks, kd: Kernel Density}''') + parser.add_argument('--path', default=None, type=str, help='''input/output path (must contain the input _max.txt and + _min.txt files in a subdirectory sn_data)''') + parser.add_argument('--unpickle', default=False, action='store_true', help="read the pickled realization instead of making a new one") + + parser.add_argument('--verbose', default=False, action='store_true', help="verbose mode") + parser.add_argument('--log', default=None, type=str, help="log file, if not passed defaults to standard output") + parser.add_argument('--nodust', default=False, action='store_true', help=" don't do dust corrections (default is to do it)") + parser.add_argument('--bluedust', default=False, action='store_true', help=" use Hg/Hb for dust correction (default is Hb/Ha)") + parser.add_argument('--noplot', default=False, action='store_true', help=" don't plot individual distributions (default is to plot all distributions)") + parser.add_argument('--asciiout', default=False, action='store_true', help=" write distribution in an ascii output (default is not to)") + parser.add_argument('--asciidistrib', default=False, action='store_true', help=" write distribution in an ascii output (default is not to)") + parser.add_argument('--md', default='all', type=str, help='''metallicity diagnostic to calculate. + default is 'all', options are: + D02, Z94, M91, C01, P05, M08, M08all, M13, PP04, D13, D13all, KD02, DP00 (deprecated), P01, D16, PG16, C17''') + parser.add_argument('--multiproc', default=False, action='store_true', help=" multiprocess, with number of threads max(available cores-1, MAXPROCESSES)") + args = parser.parse_args() + + global CLOBBER + global VERBOSE + global BINMODE + global ASCIIOUTPUT + global ASCIIDISTRIB + global NOPLOT + CLOBBER = args.clobber + VERBOSE = args.verbose + BINMODE = args.binmode + NOPLOT = args.noplot + ASCIIOUTPUT = args.asciiout + ASCIIDISTRIB = args.asciidistrib + + if args.unpickle and NOPICKLE: + args.unpickle = False + raw_input("cannot use pickle on this machine, we won't save and won't read saved pickled realizations. Ctr-C to exit, Return to continue?\n") + + if args.path: + path = args.path + else: + assert (os.getenv("MCMetdata")), ''' the _max, _min (and _med) data must live in a folder named sn_data. + pass a path to the sn_data folder, or set up the environmental variable + MCMetdata pointing to the path where sn_data lives ''' + path = os.getenv("MCMetdata") + assert(os.path.isdir(path)), "pass a path or set up the environmental variable MCMetdata pointing to the path where the _min _max _med files live" + if args.nsample == 1: + print ("CALCULATING METALLICITY WITHOUT GENERATING MC DISTRIBUTIONS") + if args.nsample == 1 or args.nsample >= 10: + fi = input_data(args.name, path=path) + if fi != -1: + logf = smart_open(args.log) + run(fi, args.nsample, args.md, args.multiproc, logf, + unpickle=args.unpickle, + dust_corr=(not args.nodust), dust_blue=(args.bluedust), + verbose=VERBOSE) + if args.log: + logf.close() + else: + print ("nsample must be at least 100") + +if __name__ == "__main__": + if PROFILING: + import cProfile + cProfile.run("main()") + else: + main() diff --git a/build/scripts-3.6/metallicity.py b/build/scripts-3.6/metallicity.py new file mode 100755 index 0000000..d5d2e79 --- /dev/null +++ b/build/scripts-3.6/metallicity.py @@ -0,0 +1,297 @@ +############################################################################## +## Calculates oxygen abundance (here called metalicity) based on strong emission lines, +## based on code originally written in IDL by Lisa Kewley (Kewley & Ellison 2008). Outputs +## oxygen abundance in many different diagnostics (see Bianco et al. 2016). +## +##new calculation based on the most recent version of the .pro file. +## +##inputs: +## measured - flux data, must be the format returned by readfile() +## num - number of spectra for which to calculate metallicity, also returned by readfile() +## outfilename - the name of the file the results will be appended to +## red_corr - reddening correction flag - True by default +## disp - if True prints the results, default False +############################################################################## + +from __future__ import print_function +import sys +import os +import numpy as np + +IGNOREDUST = False +MP = True +FIXNEGATIVES = True # set to true if no negative flux measurements should be allowed. all negative flux measurements are set to 0 +DROPNEGATIVES = False # set to true if no negative flux measurements should be allowed. all negative flux measurements are dropped +if DROPNEGATIVES: + FIXNEGATIVES = False + +##list of metallicity methods, in order calculated +Zs = ["E(B-V)", # based on Halpha, Hbeta + "E(B-V)blue", # based on Hbeta, Hgamma + "C(Hbeta)", + "logR23", # Hbeta, [OII]3727, [OIII]5007, [OIII]4959 + + "D02", # Halpha, [NII]6584 + "Z94", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "M91", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "C01_N2S2", # [OII]3727, [OIII]5007, [NII]6584, [SII]6717 + "C01_R23", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + + "P05", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "P01", # available but deprecated + "PP04_N2Ha", # Halpha, [NII]6584 + "PP04_O3N2", # Halpha, Hbeta,[OIII]5007, [NII]6584 + "DP00", # S23 available but deprecated + "P10_ONS", "P10_ON", + "M08_R23", "M08_N2Ha", "M08_O3Hb", "M08_O2Hb", "M08_O3O2", "M08_O3N2", + "M13_O3N2", "M13_N2Ha", + "D13_N2S2_O3S2", "D13_N2S2_O3Hb", + "D13_N2S2_O3O2", "D13_N2O2_O3S2", + "D13_N2O2_O3Hb", "D13_N2O2_O3O2", + "D13_N2Ha_O3Hb", "D13_N2Ha_O3O2", + "KD02_N2O2", # Halpha, Hbeta, [OII]3727, [NII]6584 + "KD02_N2S2", + "KK04_N2Ha", # Halpha, Hbeta, [OII]3727, [NII]6584 + "KK04_R23", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) + "KD02comb", + "PM14", + "PMC09_N2Ha", + "B07_N2O2", # Bresolin (2007) N2O2 + "D16", # Dopita (2016) ONS + "PG16_R","PG16_S", # Pilyugin & Grebel (2016) + "C17_O3O2", "C17_O3N2","C17_N2Ha","C17_O2Hb","C17_O3Hb","C17_R23"] + + +Zserr = ['PM14err'] # ,"KK04comb"] +#'KD02_N2O2', 'KD03new_R23', 'M91', 'KD03_N2Ha' + + +def get_keys(): + return Zs + + +def get_errkeys(): + return Zserr + + +def printsafemulti(string, logf, nps): + #this is needed because dealing with a log output with multiprocessing + #is painful. but it introduces a bunch of if checks. + if nps == 1: + logf.write(string + "\n") + #print >> logf, string + else: + print (string) + + +############################################################################## +##fz_roots function as used in the IDL code +############################################################################## + +#@profile +def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, + dust_blue=False, disp=False, verbose=False): + + global IGNOREDUST + mscales.setdustcorrect() + raw_lines = {} + raw_lines['[OIII]5007'] = np.array([float('NaN')]) + raw_lines['Hb'] = np.array([float('NaN')]) + raw_lines['Hg'] = np.array([float('NaN')]) + raw_lines['Hz'] = np.array([float('NaN')]) + for k in measured.iterkeys(): + #kills all non-finite terms + measured[k][~(np.isfinite(measured[k][:]))] = 0.0 + + if FIXNEGATIVES: + measured[k][measured[k] < 0] = 0.0 + + if DROPNEGATIVES: + measured[k][measured[k] < 0] = np.nan + + raw_lines[k] = measured[k] + ######we trust QM better than we trust the measurement of the [OIII]4959 + ######which is typical low S/N so we set it to [OIII]5007/3. + ######change this only if youre spectra are very high SNR + raw_lines['[OIII]4959'] = raw_lines['[OIII]5007'] / 3. + raw_lines['[OIII]49595007'] = raw_lines['[OIII]4959'] + raw_lines['[OIII]5007'] + mscales.setHabg(raw_lines['Ha'], raw_lines['Hb'], raw_lines['Hg']) + + #if Ha or Hb is zero, cannot do red correction + if dust_corr and mscales.hasHa and mscales.hasHb and not dust_blue: + with np.errstate(invalid='ignore'): + mscales.calcEB_V() + elif dust_blue and mscales.hasHg and mscales.hasHb: + with np.errstate(invalid='ignore'): + mscales.calcEB_Vblue() + print('----- Using Hg/Hb for E(B-V)_blue estimate. -----') + elif dust_corr and not IGNOREDUST: + + if nps > 1: + print ('''WARNING: reddening correction cannot be done + without both H_alpha and H_beta measurement!!''') + + else: + response = raw_input('''WARNING: reddening correction cannot be done without both H_alpha and H_beta measurement!! + Continuing without reddening correction? [Y/n]\n''').lower() + assert(not (response.startswith('n'))), "please fix the input file to include Ha and Hb measurements" + + IGNOREDUST = True + dust_corr = False + mscales.mds['E(B-V)'] = np.ones(len(raw_lines['Ha'])) * 1e-5 + else: + mscales.unsetdustcorrect() + mscales.mds['E(B-V)'] = np.ones(len(raw_lines['Ha'])) * 1e-5 + + for k in ['[OII]3727', '[OIII]5007', '[OI]6300', '[OIII]4959', + '[SII]6717', '[SII]6731', '[SIII]9069', '[SIII]9532' + '[OII]3727', '[OIII]5007', '[OI]6300', '[OIII]4959', + '[NII]6584', '[SIII]9532']: + if k not in raw_lines or (len(raw_lines[k]) == 1 and np.isnan(raw_lines[k][0])): + raw_lines[k] = np.array([0.] * num) + + mscales.setOlines(raw_lines['[OII]3727'], raw_lines['[OIII]5007'], raw_lines['[OI]6300'], raw_lines['[OIII]4959']) + mscales.setSII(raw_lines['[SII]6717'], raw_lines['[SII]6731'], raw_lines['[SIII]9069'], raw_lines['[SIII]9532']) + mscales.setNII(raw_lines['[NII]6584']) + + if mscales.checkminimumreq(dust_corr, IGNOREDUST) == -1: + return -1 + + mscales.calcNIIOII() + mscales.calcNIISII() + + mscales.calcR23() + #mscales.calcS23() + + mscales.initialguess() + mds = mds.split(',') + + + #mscales.printme() + if verbose: + print ("calculating metallicity diagnostic scales: ", mds) + if 'all' in mds: + mscales.calcD02() + if os.getenv("PYQZ_DIR"): + cmd_folder = os.getenv("PYQZ_DIR") + '/' + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + mscales.calcpyqz() + else: + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable : + export PYQZ_DIR="your/path/where/pyqz/resides/ in bash, for example, if you want this scale. \n''', logf, nps) + + mscales.calcZ94() + mscales.calcM91() + + mscales.calcPP04() + + #mscales.calcP05() + #mscales.calcP10() + + mscales.calcM08() + mscales.calcM13() + + mscales.calcKD02_N2O2() + mscales.calcKK04_N2Ha() + + mscales.calcKK04_R23() + mscales.calcKDcombined() + + mscales.calcD16() + mscales.calcPG16() + mscales.calcC17() + + if 'DP00' in mds: + mscales.calcDP00() + if 'P01' in mds: + mscales.calcP01() + + if 'D02' in mds: + mscales.calcD02() + if 'D13' in mds: + if os.getenv("PYQZ_DIR"): + cmd_folder = os.getenv("PYQZ_DIR") + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + #mscales.calcpyqz() + #in order to see the original pyqz plots + #call pyqz with option plot=True by + #using the commented line below instead + mscales.calcpyqz(plot=disp) + else: + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable + PYQZ_DIR if you want this scale. ''', logf, nps) + + if 'D13all' in mds: + if os.getenv("PYQZ_DIR"): + cmd_folder = os.getenv("PYQZ_DIR") + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + #mscales.calcpyqz() + #in order to see the original pyqz plots + #call pyqz with option plot=True by + #using the commented line below instead + mscales.calcpyqz(plot=disp, allD13=True) + else: + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable + PYQZ_DIR if you want this scale. ''', logf, nps) + + if 'PM14' in mds: + if os.getenv("HIICHI_DIR"): + cmd_folder = os.getenv("HIICHI_DIR") + '/' + if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + mscales.calcPM14() + if 'PP04' in mds: + mscales.calcPP04() + if 'Z94' in mds: + mscales.calcZ94() + if 'M91' in mds: + mscales.calcM91() + if 'P10' in mds: + mscales.calcP10() + if 'M13' in mds: + mscales.calcM13() + if 'M08all' in mds: + mscales.calcM08(allM08=True) + elif 'M08' in mds: + mscales.calcM08() + if 'P05' in mds: + mscales.calcP05() + if 'C01' in mds: + mscales.calcC01_ZR23() + if 'KD02' in mds: + mscales.calcKD02_N2O2() + mscales.calcKK04_N2Ha() + mscales.calcKK04_R23() + mscales.calcKDcombined() + + # Modifications by jch [5/2017, Santiago] to include newer scales. + if 'B07_N2O2' in mds: + # Bresolin 2007 N2O2 + mscales.calcB07() + if 'D16' in mds: + # Dopita+ 2016 ONS + mscales.calcD16() + if 'PG16' in mds: + # Pilyugin & Grebel 2016 + mscales.calcPG16() + if 'C17' in mds: + # Curti+ 2017 + mscales.calcC17() + if 'C17all' in mds: + # Curti+ 2017 + mscales.calcC17(allC17=True) + if 'PMC09_N2Ha' in mds: + # Perez-Montero & Contini (2009) + mscales.calcPMC09() + + if 'KK04_N2Ha' in mds: + mscales.calcKD02_N2O2() + mscales.calcKK04_N2Ha() + elif 'KD02_N2O2' in mds: + mscales.calcKD02_N2O2() diff --git a/build/scripts-3.6/metscales.py b/build/scripts-3.6/metscales.py new file mode 100755 index 0000000..6a57478 --- /dev/null +++ b/build/scripts-3.6/metscales.py @@ -0,0 +1,1558 @@ +from __future__ import print_function +import numpy as np +#import sys +import scipy.stats as stats +import numpy.polynomial.polynomial as nppoly +from metallicity import get_keys, printsafemulti + +import pdb + +niter = 5 # number of iteations+1 for KD02 methods + +k_Ha = 2.535 # CCM Rv=3.1 +k_Hb = 3.609 # CCM Rv=3.1 +k_Hg = 4.400 # CCM Rv=3.1 + +#k_O1=2.661 # CCM Rv=3.1 +k_O2 = 4.771 # CCM Rv=3.1 +k_O35007 = 3.341 # CCM Rv=3.1 +k_O34959 = 3.384 # CCM Rv=3.1 +k_O3 = (k_O35007 + k_O34959) / 2. + +k_N2 = 2.443 # CCM Rv=3.1 +k_S2 = 2.381 # CCM Rv=3.1 + +k_S3 = 1 # guess for CCM Rv=3.1 + +global DUSTCORRECT +DUSTCORRECT = True + +''' +R23_coef=np.zeros((5,7)) # coefficients from model grid fits +R23c0=[-3267,-3727.42,-4282.30,-4745.18,-4516.46,-3509.63,-1550.53] +R23_coef[:,0]=[-3267.93,1611.04,-298.187,24.5508,-0.758310] # q=5e6 +R23_coef[:,1]=[-3727.42,1827.45,-336.340,27.5367,-0.845876] # q=1e7 +R23_coef[:,2]=[-4282.30,2090.55,-383.039,31.2159,-0.954473] # q=2e7 +R23_coef[:,3]=[-4745.18,2309.42,-421.778,34.2598,-1.04411] # q=4e7 +R23_coef[:,4]=[-4516.46,2199.09,-401.868,32.6686,-0.996645] # q=8e7 +R23_coef[:,5]=[-3509.63,1718.64,-316.057,25.8717,-0.795242] # q=1.5e8 +R23_coef[:,6]=[-1550.53,784.262,-149.245,12.6618,-0.403774] # q=3e8 + + +N2S2_coef=np.zeros((5,7)) # coefficients from model grid fits +N2S2c0=[-1042.47,-1879.46,-2027.82,-2080.31,-2162.93,-2368.56,-2910.63] +N2S2_coef[:,0]=[-1042.47,521.076,-97.1578,8.00058,-0.245356] +N2S2_coef[:,1]=[-1879.46,918.362,-167.764,13.5700,-0.409872] +N2S2_coef[:,2]=[-2027.82,988.218,-180.097,14.5377,-0.438345] +N2S2_coef[:,3]=[-2080.31,1012.26,-184.215,14.8502,-0.447182] +N2S2_coef[:,4]=[-2162.93,1048.97,-190.260,15.2859,-0.458717] +N2S2_coef[:,5]=[-2368.56,1141.97,-205.908,16.4451,-0.490553] +N2S2_coef[:,6]=[-2910.63,1392.18,-249.012,19.7280,-0.583763] + +O3O2_coef=np.zeros((4,8)) # coefficients from model grid fits +O3O2c0=[-36.9772,-74.2814,-36.7948,-81.1880,-52.6367,-86.8674,-24.4044,49.4728] +O3O2_coef[:,0]=[-36.9772,10.2838,-0.957421,0.0328614] #z=0.05 +O3O2_coef[:,1]=[-74.2814,24.6206,-2.79194,0.110773] # z=0.1 +O3O2_coef[:,2]=[-36.7948,10.0581,-0.914212,0.0300472] # z=0.2 +O3O2_coef[:,3]=[-81.1880,27.5082,-3.19126,0.128252] # z=0.5 +O3O2_coef[:,4]=[-52.6367,16.0880,-1.67443,0.0608004] # z=1.0 +O3O2_coef[:,5]=[-86.8674,28.0455,-3.01747,0.108311] # z=1.5 +O3O2_coef[:,6]=[-24.4044,2.51913,0.452486,-0.0491711] # z=2.0 +O3O2_coef[:,7]=[49.4728,-27.4711,4.50304,-0.232228] # z=3.0 +''' + +M08_coefs = {'R23': [0.7462, -0.7149, -0.9401, -0.6154, -0.2524], + 'N2Ha': [-0.7732, 1.2357, -0.2811, -0.7201, -0.3330], + 'O3Hb': [0.1549, -1.5031, -0.9790, -0.0297], + 'O3O2': [-0.2839, -1.3881, -0.3172], + 'O2Hb': [0.5603, 0.0450, -1.8017, -1.8434, -0.6549], + 'O3N2': [0.4520, -2.6096, -0.7170, 0.1347]} + +C17_coefs = {'N2Ha': [-0.489, 1.513,-2.554,-5.293,-2.867], + 'O3O2': [-0.691,-2.944,-1.308], + 'O3N2': [0.281,-4.765,-2.268], + 'O2Hb': [0.418,-0.961,-3.505,-1.949], + 'O3Hb': [-0.277,-3.549,-3.593,-0.981], + 'R23': [0.527,-1.569,-1.652,-0.421]} + +#this is to check the Maiolino coefficients and find the split maximum of the cirves with degeneracy +''' +import pylab as pl + +x=np.arange(7.0,9.5,0.1) + +for k in M08_coefs.iterkeys(): + print k,max(nppoly.polyval(x-8.69,M08_coefs[k])),x[nppoly.polyval(x-8.69,M08_coefs[k])==max(nppoly.polyval(x-8.69,M08_coefs[k]))] + pl.plot(x,nppoly.polyval(x-8.69,M08_coefs[k]), label=k+' max:%.1f'%x[nppoly.polyval(x-8.69,M08_coefs[k])==max(nppoly.polyval(x-8.69,M08_coefs[k]))]) + +print nppoly.polyval(8.4-8.69,M08_coefs['N2Ha']) +pl.ylabel("log R") +pl.xlabel("12+log(O/H)") +pl.legend(loc=3) +pl.show() +''' + + +class diagnostics: + def __init__(self, num, logf, nps): + self.nm = num + self.Ha = None + self.Hb = None + self.Hg = None + + self.hasHa, self.hasHb, self.hasHg = False, False, False + self.hasO2, self.hasO3 = False, False + self.hasS2, self.hasN2 = False, False + + self.hasO3Hb = False + self.hasO3O2 = False + + self.hasN2O2 = False + self.hasN2S2 = False + + self.hasS26731 = False + self.hasS39532 = False + self.hasS39069 = False + self.hasS2Hb = False + + self.N2O2_roots = None + #other lines calculated and repeatedly used + self.P = None + self.R2 = None + self.R3 = None + self.R23 = None + self.S2Hb = None + self.N2 = None + self.N2S2 = None + self.O23727 = None + self.O35007 = None + self.N26584 = None + self.S26717 = None + self.S26731 = None + self.S39069 = None + self.S39532 = None + + self.O34959p5007 = None + self.O35007O2 = None + self.O2O35007 = None + + self.logR23 = None + self.logN2O2 = None + self.logN2S2 = None + self.logO3O2 = None + self.logS23 = None + #self.logS3S2=None + + self.logO2Hb = None + self.logO3Hb = None + self.logN2Ha = None + self.logS2Ha = None + + self.logO35007O2 = None + self.logO2O35007 = None + + self.logO3O2sq = None + self.logq = None + self.Z_init_guess = None + self.N2O2_coef0 = 1106.8660 + + self.OIII_OII = None + self.OIII_Hb = None + self.OIII_SII = None + + self.NII_OII = None + self.NII_SII = None + #metallicity diagnostics to be returned + + self.mds = {} + for Z in get_keys(): + self.mds[Z] = None + + #setting output file + self.logf = logf + self.nps = nps + + def printme(self, verbose=False): + try: + print ("\nHa", np.mean(self.Ha)) + if verbose: + print (self.Ha) + except (IndexError, TypeError): + pass + try: + print ("\nHb", np.mean(self.Hb)) + if verbose: + print (self.Hb) + except (IndexError, TypeError): + pass + try: + print ("\nO2", np.mean(self.O23727)) + if verbose: + print (self.O23727) + except (IndexError, TypeError): + pass + try: + print ("\nO3", np.mean(self.O35007)) + if verbose: + print (self.O35007) + except (IndexError, TypeError): + pass + try: + print ("\nO34959", np.mean(self.O34959)) + if verbose: + print (self.O34959) + except (IndexError, TypeError): + pass + try: + print ("\nZ94", np.mean(self.mds['Z94'])) + if verbose: + print (self.mds['Z94']) + except (IndexError, TypeError): + pass + try: + print ("\nR23", np.mean(self.R23)) + if verbose: + print (self.R23) + except (IndexError, TypeError): + pass + try: + print ("\nlog(R23)", np.mean(self.logR23)) + if verbose: + print (self.logR23) + except (TypeError, IndexError): + pass + try: + print ("\nlog([NII][OII])", stats.nanmean(self.logN2O2)) + if verbose: + print (self.logN2O2) + except (TypeError, IndexError): + pass + try: + print ("\nlog([OIII][OII])", stats.nanmean(self.logO3O2)) + if verbose: + print (self.logO3O2) + except (TypeError, IndexError): + pass + for k in self.mds.iterkeys(): + print ("\n", k) + try: + print (stats.nanmean(self.mds[k]), np.stdev(self.mds[k])) + except (IndexError, TypeError): + if verbose: + print (self.mds[k]) + + def checkminimumreq(self, red_corr, ignoredust): + if red_corr and not ignoredust: + if not self.hasHa: + return -1 + if not self.hasHb: + return -1 + #if not self.hasO2 : + # return -1 + #if not self.hasO3 : + # return -1 + #if not self.hasS2 : + # return -1 + if not self.hasN2 and not (self.hasO2 and self.hasO3): + return -1 + + def fz_roots(self, coef): + if len(coef.shape) == 1: + coef[~(np.isfinite(coef))] = 0.0 + rts = np.roots(coef[::-1]) + if rts.size == 0: + printsafemulti('WARNING: fz_roots failed', self.logf, self.nps) + rts = np.zeros(coef.size - 1) + return rts + + else: + rts = np.zeros((coef.shape[0], coef.shape[1] - 1), dtype=complex) + coef[~(np.isfinite(coef))] = 0.0 + + for i in range(coef.shape[0]): + rts[i] = np.roots(coef[i][::-1]) # ::-1][0]) + return rts + + def setdustcorrect(self): + global DUSTCORRECT + DUSTCORRECT = True + + def unsetdustcorrect(self): + global DUSTCORRECT + DUSTCORRECT = False + + def dustcorrect(self, l1, l2, flux=False): + #global DUSTCORRECT + if DUSTCORRECT: + if not flux: + return 0.4 * self.mds['E(B-V)'] * (l1 - l2) + return 10 ** (0.4 * self.mds['E(B-V)'] * (l1 - l2)) + else: + if not flux: + return 0 + return 1.0 + + def setHabg(self, Ha, Hb, Hg): + self.Ha = Ha + self.Hb = Hb + self.Hg = Hg + if sum(self.Ha > 0): + self.hasHa = True + if sum(self.Hb > 0): + self.hasHb = True + if sum(self.Hg > 0): + self.hasHg = True + + def setOlines(self, O23727, O35007, O16300, O34959): + self.O23727 = O23727 + self.O35007 = O35007 + self.O16300 = O16300 + + if sum(self.O35007 > 0): + self.hasO3 = True + if sum(self.O23727 > 0): + self.hasO2 = True + + if self.hasO2 and self.hasO3: + self.O35007O2 = (self.O35007 / self.O23727) * self.dustcorrect(k_O3, k_O2, flux=True) + self.O2O35007 = (self.O23727 / self.O35007) * self.dustcorrect(k_O2, k_O3, flux=True) + + self.logO35007O2 = np.log10(self.O35007O2) + self.logO2O35007 = np.log10(self.O2O35007) + + #self.logO2O35007Hb=np.log10((self.O23727+self.O35007)/self.Hb) + # ratios for other diagnostics - slightly different ratios needed + # commenting since it is not used + #if self.hasHb: + # self.logO2O35007Hb = np.log10( + # (self.O23727 / self.Hb) * self.dustcorrect(k_O2, + # k_Hb, flux=True) + # + (self.O35007 / self.Hb) * self.dustcorrect(k_O35007, + # k_Hb, flux=True)) + + else: + printsafemulti("WARNING: needs O lines and and Ha/b: did you run setHab()?", self.logf, self.nps) + if self.hasHb: + if self.hasO2: + self.logO2Hb = np.log10(self.O23727 / self.Hb) + \ + self.dustcorrect(k_O2, k_Hb, flux=False) + # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + if self.hasO3: + self.O3Hb = (self.O35007 / self.Hb) * \ + self.dustcorrect(k_O35007, k_Hb, flux=True) + # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + self.logO3Hb = np.log10(self.O3Hb) + self.hasO3Hb = True + + + if self.hasO2 and self.hasO3: + self.OIII_OII = np.log10(self.O35007 / self.O23727 * self.dustcorrect(k_O35007, k_O2, flux=True)) + if O34959 is not None and sum(O34959 > 0) > 0: + self.O34959p5007 = (O34959 + self.O35007) + self.logO3O2 = np.log10((self.O34959p5007) / self.O23727)\ + + self.dustcorrect(k_O3, k_O2, flux=False) + #this is useful when we get logq + self.hasO3O2 = True + if self.hasHb: + self.OIII_Hb = np.log10(self.O35007 / self.Hb * self.dustcorrect(k_O35007, k_Hb, flux=True)) + + def setNII(self, N26584): + if N26584 is not None and sum(N26584 > 0): + self.N26584 = N26584 + self.hasN2 = True + if self.hasHa: + self.logN2Ha = np.log10(self.N26584 / self.Ha) # +self.dustcorrect(k_N2,k_Ha,flux=True) + #lines are very close: no dust correction + #Note: no dust correction cause the lies are really close! + else: + printsafemulti("WARNING: needs NII6584 and Ha to calculate NIIHa: did you run setHab()?", self.logf, self.nps) + if self.hasS2 and self.hasS26731 and self.hasN2: + self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_S2,flux=True) + #lines are very close: no dust correction + if self.hasO2 and self.hasN2: + self.NII_OII = np.log10(self.N26584 / self.O23727 * self.dustcorrect(k_N2, k_O2, flux=True)) + + def setSII(self, S26717, S26731, S39069, S39532): + if S26717 is not None and sum(S26717 > 0) > 0: + self.S26717 = S26717 + self.hasS2 = True + + if self.hasHa: + self.logS2Ha = np.log10(self.S26717 / self.Ha) + self.dustcorrect(k_S2, k_Ha, flux=False) + else: + printsafemulti("WARNING: needs SII6717 and Ha to calculate SIIHa: did you run setHab() and setS()?", self.logf, self.nps) + if S26731 is not None and sum(S26731 > 1e-9) > 0: + self.S26731 = S26731 + self.hasS26731 = True + if S39069 is not None and sum(S39069 > 1e-9) > 0: + self.S39069 = S39069 + self.hasS39069 = True + if S39532 is not None and sum(S39532 > 1e-9) > 0: + self.S39532 = S39532 + self.hasS39532 = True + if self.hasS2: + if self.hasN2 and self.NII_SII is None and self.hasS26731: + self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_O2,flux=True) + #lines are very close: no dust correction + if self.hasO3 and self.OIII_SII is None and self.hasS26731: + self.OIII_SII = np.log10(self.O35007 / (self.S26717 + self.S26731) * self.dustcorrect(k_O3, k_S2, flux=True)) + + #@profile + def calcEB_V(self): + printsafemulti("calculating E(B-V)", self.logf, self.nps) + a2b_intrinsic = 2.86 + self.mds['E(B-V)'] = np.log10(a2b_intrinsic * self.Hb / self.Ha) / (0.4 * (k_Ha - k_Hb)) # E(B-V) + self.mds['E(B-V)'][self.mds['E(B-V)'] <= 0] = 1e-5 + + #@profile + def calcEB_Vblue(self): + printsafemulti("calculating E(B-V)_blue", self.logf, self.nps) + b2g_intrinsic = 2.145 + self.mds['E(B-V)'] = np.log10(b2g_intrinsic * self.Hg / self.Hb) / (0.4 * (k_Hb - k_Hg)) # E(B-V) + self.mds['E(B-V)'][self.mds['E(B-V)'] <= 0] = 1e-5 + + #@profile + def calcNIISII(self): + if self.hasS2 and self.hasN2: + self.N2S2 = self.N26584 / self.S26717 * self.dustcorrect(k_N2, k_S2, flux=True) # 0.4*self.mds['E(B-V)']*(k_N2-k_S2) + self.logN2S2 = np.log10(self.N26584 / self.S26717) + self.dustcorrect(k_N2, k_S2, flux=False) # 0.4*self.mds['E(B-V)']*(k_N2-k_S2) + self.hasN2S2 = True + else: + printsafemulti("WARNING: needs SII6717 and NII6584 to calculate NIISII: did you run setN2() and setS?", self.logf, self.nps) + + #@profile + def calcNIIOII(self): + if self.hasN2 and self.hasO2: + self.logN2O2 = np.log10(self.N26584 / self.O23727) + self.dustcorrect(k_N2, k_O2, flux=False) + + self.hasN2O2 = True + if not self.hasN2O2 or np.mean(self.logN2O2) < -1.2: + + try: + printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods should only be used for log([NII]6564/[OII]3727) > -1.2, + the mean log([NII]6564/[OII]3727)= %f''' % np.mean(self.logN2O2), self.logf, self.nps) + except TypeError: + printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods + should only be used for log([NII]6564/[OII]3727) >-1.2, + the mean log([NII]6564/[OII]3727)= %s''' % self.logN2O2, self.logf, self.nps) + + if not self.hasN2O2: + self.N2O2_roots = np.zeros(self.nm) + float('NaN') + else: + N2O2_coef = np.array([[self.N2O2_coef0, -532.15451, 96.373260, -7.8106123, 0.23928247]] * self.nm).T # q=2e7 line (approx average) + N2O2_coef[0] -= self.logN2O2 + N2O2_coef = N2O2_coef.T + # finding roots for == (4) + self.N2O2_roots = np.array([self.fz_roots(N2O2_coef)])[0] + + #@profile + def calcR23(self): + printsafemulti("calculating R23", self.logf, self.nps) + + #R23 NEW Comb, [NII]/Ha: KK04 = Kobulnicky & Kewley, 2004, submitted' + if self.hasO3 and self.hasO2 and self.hasHb: + self.R2 = (self.O23727 / self.Hb) * self.dustcorrect(k_O2, k_Hb, flux=True) + self.R3 = (self.O34959p5007 / self.Hb) * self.dustcorrect(k_O3, k_Hb, flux=True) + self.R23 = self.R2 + self.R3 + self.logR23 = np.log10(self.R23) + self.mds['logR23'] = self.logR23 + #note that values of logR23 > 0.95 are unphysical. + #you may choose to uncomment the line below + #self.logR23[self.logR23>0.95]=0.95 + else: + printsafemulti("WARNING: need O3, O2, Hb", self.logf, self.nps) + + #@profile + def calcS23(self): + printsafemulti("calculating S23", self.logf, self.nps) + #the original code here uses S267176731, + #which is however set to 6717 as default + #Vilchez & Esteban (1996) + if self.hasS2: + if self.hasS39069 and self.hasHb: + self.logS23 = np.log10((self.S26717 / self.Hb) * + self.dustcorrect(k_S2, k_Hb, flux=True) + + (self.S39069 / self.Hb) * + self.dustcorrect(k_S3, k_Hb, flux=True)) + + #self.logS3S2=np.log10(S39069/self.S26717)+self.dustcorrect(k_S3,k_S2) + + ##@profile + def calclogq(self, Z): + if not self.hasO3O2: + printsafemulti("WARNING: needs O3,O2,Hb to calculate logq properly.", self.logf, self.nps) + return -1 + if self.logO3O2sq is None: + self.logO3O2sq = self.logO3O2 ** 2 + return (32.81 - 1.153 * self.logO3O2sq + Z * (-3.396 - 0.025 * self.logO3O2 + 0.1444 * self.logO3O2sq)) / (4.603 - 0.3119 * self.logO3O2 - \ + 0.163 * self.logO3O2sq + Z * (-0.48 + 0.0271 * self.logO3O2 + 0.02037 * self.logO3O2sq)) + + ##@profile + def initialguess(self): + # Initial Guess - appearing in LK code as of Nov 2006 + # upper branch: if no lines are available, metallicity is set to 8.7 + self.Z_init_guess = np.zeros(self.nm) + 8.7 + # use [N2]/Ha + if self.hasHa and self.hasN2: + self.Z_init_guess[(self.logN2Ha < -1.3) & (self.N26584 != 0.0)] = 8.2 + self.Z_init_guess[(self.logN2Ha < -1.1) & (self.logN2Ha >= -1.3) & (self.N26584 != 0.0)] = 8.4 + #A1 KE08 + self.Z_init_guess[(self.logN2Ha >= -1.1) & (self.N26584 != 0.0)] = 8.7 + #use [N2]/[O2] + if self.hasN2 and self.hasO2: + N2O2 = np.zeros(self.nm) + float('nan') + if self.hasHb: + ###FED CHECK THIS! + N2O2 = self.N26584 * self.Ha * self.Hb * self.O23727 + if not self.hasN2O2: + printsafemulti("WARNING: must calculate logN2O2 first", self.logf, self.nps) + self.calcNIIOII() + self.Z_init_guess[(self.logN2O2 < -1.2) & (N2O2 != 0.0)] = 8.2 + # at logN2O2<-1.2 using low-Z gals, A1 KE08 + self.Z_init_guess[(self.logN2O2 >= -1.2) & (N2O2 != 0.0)] = 8.7 + + # at logN2O2>-1.2 using HII regions + + + + #######################these are the metallicity diagnostics################## + #@profile + def calcpyqz(self, plot=False, allD13=False): + printsafemulti("calculating D13", self.logf, self.nps) + + # initializing variable pyqz to avoid style issues + # (pyqz not defined is reported as error by Landscape.io w/ import in func + pyqz = None + try: + import pyqz + except ImportError: + return -1 + + #check version of pyqz + from distutils.version import StrictVersion + oldpyqz = False + if StrictVersion(pyqz.__version__) <= StrictVersion('0.5.0'): + oldpyqz = True + + if self.NII_SII is not None and allD13: + if self.OIII_SII is not None: + + if oldpyqz: + self.mds['D13_N2S2_O3S2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_SII]), 'NII/SII', 'OIII/SII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + #pyqz.get_grid_fn(Pk=5.0,calibs='GCZO', kappa =20, struct='pp') + # self.mds['D13_N2S2_O3S2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + # np.atleast_1d([self.OIII_SII])], \ + # '[NII]/[SII]+;[OIII]/[SII]+', \ + # show_plot=plot, n_plot=False, \ + # save_plot=False, verbose=False)[0].T + self.mds['D13_N2S2_O3S2'] = pyqz.interp_qz('Tot[O]+12', + [np.atleast_1d([self.NII_SII]), + np.atleast_1d([self.OIII_SII])], + '[NII]/[SII]+;[OIII]/[SII]+')[0].T + + if self.OIII_Hb is not None: + if oldpyqz: + self.mds['D13_N2S2_O3Hb'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_Hb]), 'NII/SII', 'OIII/Hb', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + # self.mds['D13_N2S2_O3SHb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + # np.atleast_1d([self.OIII_Hb])], \ + # '[NII]/[SII]+;[OIII]/Hb', \ + # show_plot=plot, n_plot=False, \ + # save_plot=False, verbose=False)[0].T + self.mds['D13_N2S2_O3SHb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/[SII]+;[OIII]/Hb')[0].T + + + + if self.OIII_OII is not None: + if oldpyqz: + self.mds['D13_N2S2_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_OII]), 'NII/SII', 'OIII/OII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2S2_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + np.atleast_1d([self.OIII_OII])], \ + '[NII]/[SII]+;[OIII]/[OII]+', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + + if self.NII_OII is not None and allD13: + if self.OIII_SII is not None: + if oldpyqz: + self.mds['D13_N2O2_O3S2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_SII]), 'NII/OII', 'OIII/SII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2O2_O3S2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_SII])], \ + '[NII]/[OII]+;[OIII]/[SII]+', \ + show_plot=plot, n_plot=False, \ + save_plot=False, verbose=False)[0].T + + + if self.OIII_Hb is not None: + if oldpyqz: + self.mds['D13_N2O2_O3Hb'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_Hb]), 'NII/OII', 'OIII/Hb', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2O2_O3Hb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/[OII]+;[OIII]/Hb')[0].T + + if self.OIII_OII is not None: + if oldpyqz: + self.mds['D13_N2O2_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_OII]), 'NII/OII', 'OIII/OII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2O2_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ + np.atleast_1d([self.OIII_OII])], \ + '[NII]/[OII]+;[OIII]/[OII]+')[0].T + if self.logN2Ha is not None: + if self.OIII_Hb is not None: + if oldpyqz: + self.mds['D13_N2Ha_O3Hb'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_Hb]), 'NII/Ha', 'OIII/Hb', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + else: + self.mds['D13_N2Ha_O3Hb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/Ha;[OIII]/Hb')[0].T + if self.OIII_OII is not None: + if oldpyqz: + self.mds['D13_N2Ha_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_OII]), 'NII/Ha', 'OIII/OII', \ + method='default', plot=plot, n_plot=False, savefig=False)[0].T + + else: + self.mds['D13_N2Ha_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.logN2Ha]), \ + np.atleast_1d([self.OIII_Hb])], \ + '[NII]/Ha;[OIII]/[OII]+')[0].T + + #@profile + def calcDP00(self): + # Diaz, A. I., & Perez-Montero, E. 2000, MNRAS, 312, 130 + # As per KD02: DP00 diagnostic systematically underestimates the + # abundance relative to the comparison abundance. + # A term is added to improve the fit according to KD02 Eq. 6 + # AVAILABLE BUT DEPRECATED + printsafemulti("calculating DP00", self.logf, self.nps) + + if self.logS23 is None: + self.calcS23() + if self.logS23 is None: + printsafemulti("WARNING: Cannot compute this without S23", self.logf, self.nps) + return -1 + self.mds['DP00'] = 1.53 * self.logS23 + 8.27 + 1.0 / (2.0 - 9.0 * self.logS23 ** 3) + + #@profile + def calcD02(self): + # [NII]/Ha Denicolo, Terlevich & Terlevich (2002), MNRAS, 330, 69 + #FED:added uncertainties + printsafemulti("calculating D02", self.logf, self.nps) + + e1 = np.random.normal(0, 0.05, self.nm) + e2 = np.random.normal(0, 0.1, self.nm) + if self.hasN2 and self.hasHa: + self.mds['D02'] = 9.12 + e1 + (0.73 + e2) * self.logN2Ha + else: + printsafemulti("WARNING: need N2Ha to do this. " + + "did you run setHab and setNII", self.logf, self.nps) + + #@profile + def calcPP04(self): + ### PP04_N2_Z, PP04_O3N2_Z Pettini & Pagel diagnostics - + ### Pettini & Pagel (2004), MNRAS, 348, L59 + # [NII]/Ha Pettini & Pagel (2004), MNRAS, 348, L59 + #discriminating lower and upper branch using [NII]/[OII] or [NII]/Ha + printsafemulti("calculating PP04", self.logf, self.nps) + if self.hasN2 and self.hasHa: + self.mds['PP04_N2Ha'] = nppoly.polyval(self.logN2Ha, [9.37, 2.03, 1.26, 0.32]) + + #FED: restricting the range as per paper + index = (self.logN2Ha > -2.5) * (self.logN2Ha < -0.3) + self.mds['PP04_N2Ha'][~index] = float('NaN') + if self.hasO3Hb: + self.mds['PP04_O3N2'] = 8.73 - 0.32 * (self.logO3Hb - self.logN2Ha) + index = (self.logO3Hb > 2) + self.mds['PP04_O3N2'][index] = float('NaN') + else: + printsafemulti("WARNING: need O3Hb for PP04_O3N2", self.logf, self.nps) + else: + printsafemulti("WARNING: need N2Ha to do this. did you run setHab and setNII", self.logf, self.nps) + + #@profile + def calcZ94(self): + ### calculating z from Kobulnicky,Kennicutt,Pizagno (1998) + ### parameterization of Zaritzky et al. (1994) + ###Z94 = Zaritsky, D., Kennicutt, R. C., & Huchra, J. P. 1994, + ###ApJ, 420, 87 + ### only valid on the upper branch of R23 (KE08 A2.4) + + printsafemulti("calculating Z94", self.logf, self.nps) + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + self.mds['Z94'] = nppoly.polyval(self.logR23, [9.265, -0.33, -0.202, -0.207, -0.333]) + self.mds['Z94'][(self.logR23 > 0.9)] = None + ## 0.9 is a conservative constraint to make sure that we are + ## only using the upper branch (i.e. 12+log(O/H)>8.4) + + def calcP(self): + if self.P is None: + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + #R3=10**self.logO349595007Hb + #R2=10**self.logO2Hb + #P = R3/(R2+R3) + self.P = self.R3 / self.R23 + + #@profile + def calcP05(self): + # #### P-method ##### + ##Pilyugin+ 2005 method. Based on [OIII],[OII], Hbeta + ##calibrated from Te method + # make sure you run setOlines() first + printsafemulti("calculating P05", self.logf, self.nps) + + if self.calcP() == -1: + return -1 + if self.Z_init_guess is None: + self.initialguess() + + Psq = self.P * self.P + + P_abund_up = (self.R23 + 726.1 + 842.2 * self.P + 337.5 * Psq) / (85.96 + 82.76 * self.P + 43.98 * Psq + 1.793 * self.R23) + P_abund_low = (self.R23 + 106.4 + 106.8 * self.P - 3.40 * Psq) / (17.72 + 6.60 * self.P + 6.95 * Psq - 0.302 * self.R23) + + self.mds['P05'] = P_abund_up + self.mds['P05'][self.Z_init_guess < 8.4] = P_abund_low[self.Z_init_guess < 8.4] + + #@profile + def calcP10(self): + # #### P-method ##### + ##Pilyugin+ 2010 method. + ##calibrated from Te method + # need Hb + #The Astrophysical Journal, Volume 720, Issue 2, pp. 1738-1751 (2010). + #Published in Sep 2010 + + printsafemulti("calculating P10", self.logf, self.nps) + + if not self.hasHb: + printsafemulti("this method needs Hb", self.logf, self.nps) + return -1 + self.mds['P10_ONS'] = np.zeros(self.nm) + float('NaN') + self.mds['P10_ON'] = np.zeros(self.nm) + float('NaN') + #P10N2=np.zeros(self.nm)+float('NaN') + #P10S2=np.zeros(self.nm)+float('NaN') + P10logR3 = np.zeros(self.nm) + float('NaN') + P10logR2 = np.zeros(self.nm) + float('NaN') + P10logN2 = np.zeros(self.nm) + float('NaN') + P10logS2 = np.zeros(self.nm) + float('NaN') + + self.calcP() + if self.R2 is not None: + P10logR2 = np.log(self.R2) + + if self.R3 is not None: + P10logR3 = np.log(self.R3) + + if self.hasN2: + #the ratio of N26548 and N26548 is N26584/N26548 = 3 + #independent on physical conditions + #The Physics and Dynamics of Planetary Nebulae + # By Grigor A. Gurzadyan + P10logN2 = (np.log((self.N26584 * 1.33) / self.Hb) + + self.dustcorrect(k_N2, k_Hb, flux=False)) + + if self.hasS2 and self.hasS26731: + self.S2Hb = ((self.S26717 + self.S26731) / self.Hb + * self.dustcorrect(k_S2, k_Hb, flux=True)) + self.hasS2Hb = True + P10logS2 = np.log10(self.S2Hb) + + P10logN2S2 = P10logN2 - P10logS2 + P10logN2R2 = P10logN2 - P10logR2 + P10logS2R2 = P10logS2 - P10logR2 + + coefsONS0 = np.array([8.277, 0.657, -0.399, -0.061, 0.005]) + coefsONS1 = np.array([8.816, -0.733, 0.454, 0.710, -0.337]) + coefsONS2 = np.array([8.774, -1.855, 1.517, 0.304, 0.328]) + + vsONS = np.array([np.ones(self.nm), self.P, P10logR3, P10logN2R2, P10logS2R2]).T + + coefsON0 = np.array([8.606, -0.105, -0.410, -0.150]) + coefsON1 = np.array([8.642, 0.077, 0.411, 0.601]) + coefsON2 = np.array([8.013, 0.905, 0.602, 0.751]) + + vsON = np.array([np.ones(self.nm), P10logR3, P10logR2, P10logN2R2]).T + + indx = P10logN2 > -0.1 + if self.P is not None: + self.mds['P10_ONS'][indx] = np.dot(vsONS[indx], coefsONS0) + self.mds['P10_ON'][indx] = np.dot(vsON[indx], coefsON0) + + indx = (P10logN2 < -0.1) * (P10logN2S2 > -0.25) + if self.P is not None: + self.mds['P10_ONS'][indx] = np.dot(vsONS[indx], coefsONS1) + self.mds['P10_ON'][indx] = np.dot(vsON[indx], coefsON1) + + indx = (P10logN2 < -0.1) * (P10logN2S2 < -0.25) + if self.P is not None: + self.mds['P10_ONS'][indx] = np.dot(vsONS[indx], coefsONS2) + self.mds['P10_ON'][indx] = np.dot(vsON[indx], coefsON2) + + indx = ~((self.mds['P10_ONS'] > 7.1) * (self.mds['P10_ON'] > 7.1) * (self.mds['P10_ONS'] < 9.4) * (self.mds['P10_ON'] < 9.4)) + if self.P is not None: + self.mds['P10_ONS'][indx] = float('NaN') + self.mds['P10_ON'][indx] = float('NaN') + + #@profile + def calcP01(self): + # P-method 2001 upper branch (derecated and commented out) + # Pilyugin 2001 + # available but deprecated + printsafemulti("calculating old P05", self.logf, self.nps) + + if self.Z_init_guess is None: + self.initialguess() + if self.hasO3O2 and self.hasO3 and self.hasO2: + P = 10 ** self.logO3O2 / (1 + 10 ** self.logO3O2) + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + Psq = P ** 2 + P_abund_old = (self.R23 + 54.2 + 59.45 * P + 7.31 * Psq) / (6.07 + 6.71 * P + 0.371 * Psq + 0.243 * self.R23) + self.mds['P01'] = np.zeros(self.nm) + float('NaN') + self.mds['P01'][self.Z_init_guess >= 8.4] = P_abund_old[self.Z_init_guess >= 8.4] + else: + printsafemulti("WARNING: need OIIIOII to calculate P01, did you set them up with setOlines()?", self.logf, self.nps) + + #@profile + def calcC01_ZR23(self): + # C01 = Charlot, S., & Longhetti, M., 2001, MNRAS, 323, 887 + # Charlot 01 R23 calibration: (case F) ## + # available but deprecated + printsafemulti("calculating C01", self.logf, self.nps) + + if self.hasO3 and self.hasO2 and self.hasO3Hb: + x2 = self.O2O35007 / 1.5 + x3 = (10 ** self.logO3Hb) * 0.5 + self.mds['C01_R23'] = np.zeros(self.nm) + float('NaN') + self.mds['C01_R23'][self.O2O35007 < 0.8] = np.log10(3.78e-4 * (x2[self.O2O35007 < 0.8]) ** 0.17 * x3[self.O2O35007 < 0.8] ** (-0.44)) + 12.0 + + self.mds['C01_R23'][self.O2O35007 >= 0.8] = np.log10(3.96e-4 * x3[self.O2O35007 >= 0.8] ** (-0.46)) + 12.0 + else: + printsafemulti('''WARNING: need [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, +did you set them up with setOlines()?''', self.logf, self.nps) + + # Charlot 01 calibration: (case A) based on [N2]/[SII]## + # available but deprecated + if not self.hasN2S2: + printsafemulti("WARNING: trying to calculate logNIISII", self.logf, self.nps) + self.calcNIISII() + if self.hasN2S2 and self.hasO3 and self.hasO2 and self.hasO3Hb: + self.mds['C01_N2S2'] = np.log10(5.09e-4 * (x2 ** 0.17) * ((self.N2S2 / 0.85) ** 1.17)) + 12 + else: + printsafemulti('''WARNING: needs [NII]6584, [SII]6717, [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, +did you set them up with setOlines() and ?''', self.logf, self.nps) + + #@profile + def calcM91(self): + # ## calculating McGaugh (1991) + # McGaugh, S.S., 1991, ApJ, 380, 140' + # M91 calibration using [N2O2] as + # initial estimate of abundance: + # this initial estimate can be + # changed by replacing + # OH_init by another guess, eg C01_Z + # NOTE: occasionally the M91 + # 'upper branch' will give a metallicity + # that is lower than the 'lower branch'. + # Happens for very high R23 values. + # If R23 is higher than the intersection + # (calculate the intersection), then + # the metallicity is likely to be around + # the R23 maximum = 8.4 + + printsafemulti("calculating M91", self.logf, self.nps) + self.mds['M91'] = np.zeros(self.nm) + float('NaN') + + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + return -1 + + if self.Z_init_guess is None: + self.initialguess() + + M91_Z_low = nppoly.polyval(self.logR23, [12.0 - 4.944, 0.767, 0.602]) - \ + self.logO3O2 * nppoly.polyval(self.logR23, [0.29, 0.332, -0.331]) + M91_Z_up = nppoly.polyval(self.logR23, [12.0 - 2.939, -0.2, -0.237, -0.305, -0.0283]) - \ + self.logO3O2 * nppoly.polyval(self.logR23, [0.0047, -0.0221, -0.102, -0.0817, -0.00717]) + + indx = (np.abs(self.logO3O2) > 0) * (np.abs(self.logR23) > 0) * (self.Z_init_guess < 8.4) + self.mds['M91'][indx] = M91_Z_low[indx] + indx = (np.abs(self.logO3O2) > 0) * (np.abs(self.logR23) > 0) * (self.Z_init_guess >= 8.4) + self.mds['M91'][indx] = M91_Z_up[indx] + self.mds['M91'][(M91_Z_up < M91_Z_low)] = float('NaN') + + #@profile + def calcM13(self): + #Marino+ 2013 + printsafemulti("calculating M13", self.logf, self.nps) + + if not self.hasHa or not self.hasN2: + printsafemulti("WARNING: need O3, N2, Ha and Hb, " + + "or at least N2 and Ha", self.logf, self.nps) + return -1 + else: + e1 = np.random.normal(0, 0.027, self.nm) + e2 = np.random.normal(0, 0.024, self.nm) + self.mds["M13_N2Ha"] = 8.743 + e1 + (0.462 + e2) * self.logN2Ha + if self.hasHb and self.hasO3: + e1 = np.random.normal(0, 0.012, self.nm) + e2 = np.random.normal(0, 0.012, self.nm) + O3N2 = self.logO3Hb - self.logN2Ha + self.mds["M13_O3N2"] = 8.533 + e1 - (0.214 + e1) * O3N2 + index = (O3N2 > 1.7) + self.mds["M13_O3N2"][index] = float('NaN') + index = (O3N2 < -1.1) + self.mds["M13_O3N2"][index] = float('NaN') + + #for i in range(len(self.mds["M13_O3N2"])): + #print ("here O3N2",#self.O35007[i], self.Hb[i], + #self.O3Hb[i], + #self.logO3Hb[i], self.logN2Ha[i], + # O3N2[i], + # self.mds["M13_O3N2"][i]) + + #@profile + def calcM08(self, allM08=False): + #Maiolino+ 2008 + #Astronomy and Astrophysics, Volume 488, Issue 2, 2008, pp.463-479 + #Published in Sep 2008 + printsafemulti("calculating M08", self.logf, self.nps) + highZ = None + if self.logO35007O2 is not None: + self.mds['M08_O3O2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O3O2']] * self.nm).T + coefs[0] = coefs[0] - self.logO35007O2 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + #the two cumsum assure that if the condition for the ith element + #of indx is [False, False] then after the first cumsum(1) is [0,0] + #[False, True] is [0,1] + #[True, True] is [1,2] + #but (here is the kicker) [True, False] is [1,1]. + #Because i want only one solution + #(i'll settle for the first one occurring) [1,1] is ambiguous. + #The second cumsum(1) makes + #[0,0]->[0,0], [0,1]->[0,1], [1,2]->[1,3] and finally [1,1]->[1,2] + + self.mds['M08_O3O2'][(indx.sum(1)) > 0] = sols[indx].real + highZ = np.median(self.logO35007O2) < 0 + if self.logN2Ha is not None: + self.mds['M08_N2Ha'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['N2Ha']] * self.nm).T + coefs[0] = coefs[0] - self.logN2Ha + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_N2Ha'][(indx.sum(1)) > 0] = sols[indx].real + if highZ is None: + highZ = np.median(self.logN2Ha) > -1.3 + + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute M08_R23 without R23", self.logf, self.nps) + else: + self.mds['M08_R23'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['R23']] * self.nm).T + coefs[0] = coefs[0] - self.logR23 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real >= 8.0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_R23'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real <= 8.0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_R23'][(indx.sum(1)) > 0] = sols[indx].real + if not allM08: + return + else: + printsafemulti("calculating other M08s", self.logf, self.nps) + + if self.logO3Hb is not None: + + self.mds['M08_O3Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O3Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO3Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real >= 7.9)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O3Hb'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real <= 7.9)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O3Hb'][(indx.sum(1)) > 0] = sols[indx].real + + if self.logO2Hb is not None: + self.mds['M08_O2Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O2Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO2Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real >= 8.7)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= 7.1) * (sols.real <= 9.4) * (sols.imag == 0) * (sols.real <= 8.7)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + + + + if self.hasO3 and self.hasN2: + self.mds['M08_O3N2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([M08_coefs['O3N2']] * self.nm).T + coefs[0] = coefs[0] - np.log10(self.O35007 / self.N26584) \ + + self.dustcorrect(k_O35007, k_N2, flux=False) + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= 7.1) * (sols.real <= 9.4)\ + * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['M08_O3N2'][(indx.sum(1)) > 0] = sols[indx].real + + #@profile + def calcKD02_N2O2(self): + ## Kewley & Dopita (2002) estimates of abundance + ## KD02 + # KD02 [N2]/[O2] estimate (can be used for whole log(O/H)+12 range, + # but rms scatter increases to 0.11 rms for log(O/H)+12 < 8.6 + # rms = 0.04 for + # log(O/H)+12 > 8.6 + # uses equation (4) from KD02 paper + # FED: i vectorized the hell out of this function!!! + # from a 7 dimensional if/for loop to 1 if and 1 for :-D + #vectorizing makes fed happy ... + + printsafemulti("calculating KD02_N2O2", self.logf, self.nps) + + if self.hasN2 and self.hasO2 and self.hasHa and self.hasHb: + self.mds['KD02_N2O2'] = np.zeros(self.nm) + float('NaN') + if not self.hasN2O2: + printsafemulti("WARNING: must calculate logN2O2 first", self.logf, self.nps) + self.calcNIIOII() + if not self.hasN2O2 or self.N2O2_roots is None or sum(np.isnan(self.N2O2_roots.flatten())) == len(self.N2O2_roots.flatten()): + printsafemulti("WARNING: cannot calculate N2O2", self.logf, self.nps) + return -1 + roots = self.N2O2_roots.T + for k in range(4): + indx = (abs(roots[k]) >= 7.5) * (abs(roots[k]) <= 9.4) * (roots[k][:].imag == 0.0) + self.mds['KD02_N2O2'][indx] = abs(roots[k][indx]) + else: + printsafemulti("WARNING: need NII6584 and OII3727 and Ha and Hb to calculate this. did you run setO() setHab() and setNII()?", self.logf, self.nps) + return 1 + + #@profile + def calcKK04_N2Ha(self): + # calculating [N2]/Ha abundance estimates using [O3]/[O2] also + printsafemulti("calculating KK04_N2Ha", self.logf, self.nps) + + if self.mds['KD02_N2O2'] is None: + self.calcKD02_N2O2() + if self.mds['KD02_N2O2'] is None or sum(np.isnan(self.mds['KD02_N2O2'])) == self.nm: + printsafemulti("WARNING: without KD02_N2O2 cannot calculate KK04_N2Ha properly, but we will do our best...", self.logf, self.nps) + Z_new_N2Ha = np.zeros(self.nm) + 8.6 + else: + Z_new_N2Ha = self.mds['KD02_N2O2'].copy() # was 8.6 + + if self.hasN2 and self.hasHa: + logq_save = np.zeros(self.nm) + convergence, tol, ii = 100, 1.0e-3, 0 + if self.hasO3O2: + # calculating logq using the [N2]/[O2] + # metallicities for comparison + while convergence > tol and ii < 100: + ii += 1 + self.logq = self.calclogq(Z_new_N2Ha) + Z_new_N2Ha = nppoly.polyval(self.logN2Ha, [7.04, 5.28, 6.28, 2.37]) - \ + self.logq * nppoly.polyval(self.logN2Ha, [-2.44, -2.01, -0.325, +0.128]) + \ + 10 ** (self.logN2Ha - 0.2) * self.logq * (-3.16 + 4.65 * self.logN2Ha) + convergence = np.abs(self.logq - logq_save).mean() + logq_save = self.logq.copy() + if ii >= 100: + printsafemulti("WARNING [KK04_N2Ha]: loop did not converge", self.logf, self.nps) + Z_new_N2Ha = np.zeros(self.nm) + float('NaN') + else: + self.logq = 7.37177 * np.ones(self.nm) + Z_new_N2Ha = nppoly.polyval(self.logN2Ha, [7.04, 5.28, 6.28, 2.37]) - \ + self.logq * nppoly.polyval(self.logN2Ha, [-2.44, -2.01, -0.325, +0.128]) + \ + 10 ** (self.logN2Ha - 0.2) * self.logq * (-3.16 + 4.65 * self.logN2Ha) + self.mds['KK04_N2Ha'] = Z_new_N2Ha + indx = self.logN2Ha > 0.8 + self.mds['KK04_N2Ha'][indx] = float('NaN') + else: + printsafemulti("WARNING: need NII6584 and Ha to calculate this. did you run setHab() and setNII()?", self.logf, self.nps) + + #@profile + def calcKK04_R23(self): + # Kobulnicky & Kewley 2004 + # calculating upper and lower metallicities for objects without + # Hb and for objects without O3 and/or O2 + + printsafemulti("calculating KK04_R23", self.logf, self.nps) + #this is in the original code but not used :( + #if self.hasN2 and self.hasHa: + #logq_lims=[6.9,8.38] + #logN2Ha=np.log10(self.N26584/self.Ha) CHECK!! why remove dust correction?? + #Z_new_N2Ha_lims= np.atleast_2d([1.0,1.0]).T*nppoly.polyval(self.logN2Ha,[7.04, 5.28,6.28,2.37])- + #np.atleast_2d( logq_lims).T*nppoly.polyval(self.logN2Ha,[-2.44,-2.01,-0.325,0.128])+ + #np.atleast_2d(logq_lims).T*(10**(self.logN2Ha-0.2)*(-3.16+4.65*self.logN2Ha)) + # R23 diagnostics from Kobulnicky & Kewley 2004 + + Zmax = np.zeros(self.nm) + # ionization parameter form logR23 + if not self.hasO3O2: + logq = np.zeros(self.nm) + else: + if self.Z_init_guess is None: + self.initialguess() + Z_new = self.Z_init_guess.copy() + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute this without R23", self.logf, self.nps) + else: + logqold, convergence, ii = np.zeros(self.nm) + 100, 100, 0 + tol = 1e-4 + #3 iterations are typically enought to achieve convergence KE08 A2.3 + while convergence > tol and ii < 100: + Zmax = Zmax * 0.0 + ii += 1 + logq = self.calclogq(Z_new) + Zmax[(logq >= 6.7) * (logq < 8.3)] = 8.4 + # maximum of R23 curve: + Z_new = nppoly.polyval(self.logR23, [9.72, -0.777, -0.951, -0.072, -0.811]) - \ + logq * nppoly.polyval(self.logR23, [0.0737, -0.0713, -0.141, 0.0373, -0.058]) + indx = self.Z_init_guess <= Zmax + Z_new[indx] = nppoly.polyval(self.logR23[indx], [9.40, 4.65, -3.17]) - \ + logq[indx] * nppoly.polyval(self.logR23[indx], [0.272, 0.547, -0.513]) + convergence = np.abs((logqold - logq).mean()) + logqold = logq.copy() + if ii >= 100: + printsafemulti("WARNING [KK04_R23]: loop did not converge", self.logf, self.nps) + Z_new = np.zeros(self.nm) + float('NaN') + Z_new_lims = [nppoly.polyval(self.logR23, [9.40, 4.65, -3.17]) - \ + logq * nppoly.polyval(self.logR23, [0.272, 0.547, -0.513]), + nppoly.polyval(self.logR23, [9.72, -0.777, -0.951, -0.072, -0.811]) - \ + logq * nppoly.polyval(self.logR23, [0.0737, -0.0713, -0.141, 0.0373, -0.058])] + Z_new[(Z_new_lims[0] > Z_new_lims[1])] = None + self.mds['KK04_R23'] = Z_new + + #@profile + def calcKDcombined(self): + # KD02comb Kewley, L. J., & Dopita, M. A., 2002, ApJ + # updated in KE08 + # ### KD02 [NII]/[OII] estimate ### + # (can be used for log(O/H)+12 > 8.6 only) + + printsafemulti("calculating KD_combined", self.logf, self.nps) + + #We first use the + #[N ii]/[O ii] ratio to determine whether it lies on the upper + #or lower R23 branch + + if self.mds['KD02_N2O2'] is None: + self.calcKD02_N2O2() + if self.mds['KK04_N2Ha'] is None: + self.calcKK04_N2Ha() + if self.logR23 is None: + self.calcR23() + if self.mds['M91'] is None: + printsafemulti("WARNING: Must first calculate M91", self.logf, self.nps) + self.calcM91() +# if self.mds['Z94'] is None: +# printsafemulti( "WARNING: Must first calculate Z94",self.logf,self.nps) +# self.calcZ94() + if self.mds['KK04_R23'] is None: + printsafemulti("WARNING: Must first calculate KK04_R23", self.logf, self.nps) + self.calcKK04_R23() + if not self.hasHa and not self.hasHb: + printsafemulti("WARNING: need Ha and Hb for this. did you run setHab()?", self.logf, self.nps) + + #alternative way to calculate KD02_N2O2, stated in the paper KD02, + #valid in high Z regimes (Z>8.4) + #but we forego it + #if not self.logN2O2 is None: + # self.mds['KD02_N2O2']=np.log10(8.511e-4*(1.54020+1.26602*self.logN2O2+0.167977*self.logN2O2**2))+12. + #else: self.mds['KD02_N2O2']=np.zeros(self.nm)+float('NaN') + + # ionization parameter + # calculate an initial ionization parameter by assuming + # a nominal lower branch [12 + log (O/H ) = 8.2] + # or upper branch [12 + log (O/H ) = 8.7] metallicity using + # equation (13) from KK04 + logq = np.zeros(self.nm) + if self.hasN2 and self.hasO2 and self.hasHb and self.hasHa and self.hasO3O2: + logq = self.calclogq(self.mds['KD02_N2O2']) + logq[self.mds['KD02_N2O2'] >= 8.4] = self.logq[self.mds['KD02_N2O2'] >= 8.4] + else: + if self.Z_init_guess is None: + self.initialguess() + logq = self.calclogq(self.Z_init_guess) + #FED: CHECK: the paragraph below makes sense in words but i dont see where it is enforced. + # if log([NII]/[OII]) after extinction correction is <-1.5, then check the data. + # if it is only slightly less than 1.5, then this can be a result of either noisy + # data, inaccurate fluxes or extinction correction, or a higher ionization parameter + # than modelled. + # For these cases, the average of the M91,Z94 and C01 should be used. + + # KD02 R23 estimate (not reliable for 8.4 < log(O/H)+12 < 8.8) + # uses [NII]/[OII] estimate as initial guess - this can be changed below + + self.mds['KD02comb'] = np.zeros(self.nm) + float('NaN') + + indx_ig = self.Z_init_guess > 8.4 + if self.mds['KD02_N2O2'] is not None: + self.mds['KD02comb'][indx_ig] = self.mds['KD02_N2O2'][indx_ig].copy() + if self.mds['KK04_N2Ha'] is not None: + self.mds['KD02comb'][~indx_ig] = self.mds['KK04_N2Ha'][~indx_ig].copy() + if self.mds['KK04_R23'] is not None and self.mds['M91'] is not None: + # if [NII]/[OII] abundance available + # and [NII]/Ha abundance < 8.4, then use R23. + indx = (~np.isnan(self.mds['KK04_R23'])) * (~np.isnan(self.mds['M91'])) * (~indx_ig) + self.mds['KD02comb'][indx] = 0.5 * (self.mds['KK04_R23'][indx].copy() + self.mds['M91'][indx].copy()) + + else: + printsafemulti("WARNING: cannot calculate KK04comb because KK04_R23 or M91, failed", self.logf, self.nps) + +#######################these are the metallicity diagnostics################## +#@profile + def calcPM14(self): + # Perez-Montero 2014 + # (can be used for for log(O/H)+12 > 8.6 only) + import os + from subprocess import Popen, PIPE, STDOUT + from StringIO import StringIO + + printsafemulti("calculating HIICHI", self.logf, self.nps) + fin_hii_chi = open(os.getenv('HIICHI_DIR') + '/in.tmp', 'w') + + if not self.hasHb: + printsafemulti("cannot calculate HIICHI without Hbeta", self.logf, self.nps) + return -1 + + ratios = np.zeros((5, self.nm)) + + if self.R2 is not None: + ratios[0] = self.R2 + elif self.hasO2: + ratios[0] = ((self.O23727 / self.Hb) + * self.dustcorrect(k_O2, k_Hb, flux=True)) + else: + ratios[0] = np.array(['0 '] * self.nm) + + #we will never have 4363... + ratios[1] = np.zeros(self.nm) + + if self.hasO3Hb: + ratios[2] = self.O3Hb + elif self.hasO3: + ratios[2] = ((self.O35007 / self.Hb) + * self.dustcorrect(k_O35007, k_Hb, flux=True)) # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + else: + ratios[2] = np.zeros(self.nm) + + if self.hasN2: + ratios[3] = self.N26584 / self.Hb + else: + ratios[3] = np.zeros(self.nm) + + if self.hasS2Hb: + ratios[4] = self.S2Hb + elif self.hasS2 and self.hasS26731: + ratios[4] = (((self.S26717 + self.S26731) / self.Hb) * + self.dustcorrect(k_S2, k_Hb, flux=True)) + else: + ratios[4] = np.zeros(self.nm) + + + for ni in range(self.nm): + fin_hii_chi.write('%f %f %f %f %f\n' % (ratios[0][ni], ratios[1][ni], ratios[2][ni], ratios[3][ni], ratios[4][ni])) + fin_hii_chi.close() + os.system("ln -s %s/C13*dat . " % os.getenv('HIICHI_DIR')) + print ("\n\n\n\n\n") + #os.system("python %s/HII-CHI-mistry_v01.2.py in.tmp"%os.getenv('HIICHI_DIR')) + #os.system("python %s/HII-CHI-mistry_v01.2.py %s/in.tmp"%(os.getenv('HIICHI_DIR'),os.getenv('HIICHI_DIR'))) + p = Popen(['python', '%s/HII-CHI-mistry_v01.2.py' % os.getenv('HIICHI_DIR'), + '%s/in.tmp' % os.getenv('HIICHI_DIR')], + stdout=PIPE, stdin=PIPE, stderr=STDOUT) + out, err = p.communicate(input='%s/in.tmp' % os.getenv('HIICHI_DIR')) + print ("\n\n\n\n\n") + out = StringIO(out) + # for l in enumerate(out): + # if l[0].isdigit(): + # break + out = out.readlines()[12:] + self.mds['PM14'] = np.zeros((self.nm)) + self.mds['PM14err'] = np.zeros((self.nm)) + for i, l in enumerate(out): + self.mds['PM14'][i], self.mds['PM14err'][i] = map(float, l.split()[3:5]) + #data = np.loadtxt(out, skiprows=12, usecols=(3,4))#, dtype=[('lOH','f'),('elOH','f')], delimiter=",", unpack = True) + print (self.mds) + os.system('rm -r C13*dat') + + +########## jch additions + + def calcPMC09(self): + #Perez-Montero & Contini (2009) - N2Ha + printsafemulti("calculating PMC09", self.logf, self.nps) + + if not self.hasHa or not self.hasN2 or not self.hasHa: + printsafemulti("WARNING: need N2, Ha ", + self.logf, self.nps) + return -1 + PMC09_N2 = lambda x: 9.07+0.79*x + + y = 9.07 + 0.78*self.logN2Ha + self.mds["PMC09_N2Ha"] = y + + #@profile + def calcB07(self): + #Bresolin 2007 - N2O2 + printsafemulti("calculating B07", self.logf, self.nps) + + if not self.hasHa or not self.hasN2 or not self.hasO2: + printsafemulti("WARNING: need N2, O2 ", + self.logf, self.nps) + return -1 + + # Use [N2/Ha * Ha/Hb] / [O2/Hb] + x = (self.logN2Ha+np.log10(2.86)) - self.logO2Hb + + y = 8.66 + 0.36*self.logN2O2 - 0.17*self.logN2O2**2 + self.mds["B07_N2O2"] = y + index = (self.logN2O2 < -1.2) + self.mds["B07_N2O2"][index] = float('NaN') + index = (self.logN2O2 > 0.6) + self.mds["B07_N2O2"][index] = float('NaN') + + + #@profile + def calcD16(self): + #Dopita+ 2016 + printsafemulti("calculating D16", self.logf, self.nps) + + if not self.hasHa or not self.hasN2 or not self.hasS2: + printsafemulti("WARNING: need N2, Ha and SII, ", + self.logf, self.nps) + return -1 + + d16_n2s2 = np.log10(self.N26584 / (self.S26717+self.S26731)) + self.dustcorrect(k_N2, k_S2, flux=False) + y = d16_n2s2 + 0.264 * self.logN2Ha + self.mds["D16"] = 8.77 + y# + 0.45 * pow(y + 0.3, 5) + index = (y < -1.) + self.mds["D16"][index] = float('NaN') + index = (y > 0.5) + self.mds["D16"][index] = float('NaN') + + + def calcC17(self, allC17=False): + # Curti+ 2017 + # Monthly Notices Royal Astronomical Society, vol. 465, pp 1384-1400 + # Published in October 2016 + # + # Added 5/16/17 by jch, Santiago + import pdb + + valid_upper = 8.85 + valid_lower = 7.6 + + printsafemulti("calculating C17", self.logf, self.nps) + highZ = None + + if self.logO35007O2 is not None: + # C17 O3O2 + self.mds['C17_O3O2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O3O2']] * self.nm).T + coefs[0] = coefs[0] - self.logO35007O2 + + # sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + + # Find valid solutions + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + + # the two cumsum assure that if the condition for the ith element + # of indx is [False, False] then after the first cumsum(1) is [0,0] + # [False, True] is [0,1] + # [True, True] is [1,2] + # but (here is the kicker) [True, False] is [1,1]. + # Because i want only one solution + # (i'll settle for the first one occurring) [1,1] is ambiguous. + # The second cumsum(1) makes + # [0,0]->[0,0], [0,1]->[0,1], [1,2]->[1,3] and finally [1,1]->[1,2] + + # Set the results where appropriate + self.mds['C17_O3O2'][(indx.sum(1)) > 0] = sols[indx].real + + # Now use this to see if we're on the high-Z branch of R23 + highZ = np.median(self.logO35007O2) < 0 + + if self.logN2Ha is not None: + # C17 N2Ha + self.mds['C17_N2Ha'] = np.zeros(self.nm) + float('NaN') + + coefs = np.array([C17_coefs['N2Ha']] * self.nm).T + coefs[0] = coefs[0] - self.logN2Ha + + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + + self.mds['C17_N2Ha'][(indx.sum(1)) > 0] = sols[indx].real + if highZ is None: + highZ = np.median(self.logN2Ha) > -1.3 + + if self.hasO3 and self.hasN2: + # C17 O3N2 + self.mds['C17_O3N2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O3N2']] * self.nm).T + o3n2_value = self.logO3Hb - self.logN2Ha + coefs[0] = coefs[0] - o3n2_value + + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) \ + * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O3N2'][(indx.sum(1)) > 0] = sols[indx].real + + if self.logO3Hb is not None: + # C17 O3Hb + self.mds['C17_O3Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O3Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO3Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + + # C17 define this only for the high-metal branch. + indx = ((sols.real >= 8.2) * (sols.real <= valid_upper) * + (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O3Hb'][(indx.sum(1)) > 0] = sols[indx].real + + # Require allC17 flag if we want everything. + if not allC17: + return + else: + printsafemulti("calculating other C17s", self.logf, self.nps) + + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute C17_R23 without R23", self.logf, self.nps) + else: + self.mds['C17_R23'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['R23']] * self.nm).T + coefs[0] = coefs[0] - self.logR23 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0) * + (sols.real >= 8.4)).cumsum(1).cumsum(1) == 1 + self.mds['C17_R23'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0) * + (sols.real < 8.4)).cumsum(1).cumsum(1) == 1 + self.mds['C17_R23'][(indx.sum(1)) > 0] = sols[indx].real + + + if self.logO2Hb is not None: + self.mds['C17_O2Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O2Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO2Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= valid_lower) * (sols.real <= 8.3) * + (sols.imag == 0) * + (sols.real >= 8.7)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= valid_lower) * (sols.real <= 8.3) * + (sols.imag == 0) * + (sols.real <= valid_upper)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + + + + def calcPG16(self): + # Pilyugin & Grebel (2016, MNRAS, 457, 3678) + printsafemulti("calculating PG16", self.logf, self.nps) + + # Initialize the PG16 results + self.mds['PG16'] = np.zeros(self.nm) + float('NaN') + + # Make sure the lines are all here... + if not self.hasHa or not self.hasN2 or not self.hasO2 or not self.hasO3 or not self.hasS2: + printsafemulti("WARNING: need O2, O3, N2, S2, and Hb", + self.logf, self.nps) + return -1 + + pilR2 = self.logO2Hb + pilR3 = self.logO3Hb+np.log10(1.33) # Both members of doublet + pilR3R2 = (pilR3-pilR2) + + # Calculate S2/Hb = S2/Ha * Ha/Hb = S2/Ha*2.85 + pilS2 = np.log10((self.S26731 + self.S26731) / self.Ha * 2.85) + pilS2 += np.log10(self.dustcorrect(k_S2,k_Ha,flux=True)) + pilR3S2 = (pilR3-pilS2) + + # Calculate N2/Hb = N2/Ha * Ha/Hb = N2/Ha*2.85 + pilN2 = np.log10((self.N26584*1.33 / self.Ha)*2.85) + # pilN2 += np.log10(self.dustcorrect(k_N2,k_Hb,flux=True)) + + # Determine which branch we're on + highZ = (np.median(pilN2) >= -0.6) + print(np.median(pilN2)) + + # Calculate R = O2O3N2 abundances + if highZ == True: + print('PG16 high metal branch') + self.mds['PG16_R'] = 8.589+0.022*pilR3R2+0.399*pilN2 + self.mds['PG16_R'] += (-0.137+0.164*pilR3R2+0.589*pilN2)*pilR2 + else: + print('PG16 low metal branch') + self.mds['PG16_R'] = 7.932+0.944*pilR3R2+0.695*pilN2 + self.mds['PG16_R'] += (0.970-0.291*pilR3R2-0.019*pilN2)*pilR2 + + # Calculate S = S2O3N2 abundances + if highZ == True: + print('PG16 high metal branch') + self.mds['PG16_S'] = 8.424+0.030*pilR3S2+0.751*pilN2 + self.mds['PG16_S'] += (-0.349+0.182*pilR3S2+0.508*pilN2)*pilS2 + else: + print('PG16 low metal branch') + self.mds['PG16_S'] = 8.072+0.789*pilR3S2+0.726*pilN2 + self.mds['PG16_S'] += (1.069-0.17*pilR3S2+0.022*pilN2)*pilS2 diff --git a/build/scripts-3.6/pylabsetup.py b/build/scripts-3.6/pylabsetup.py new file mode 100755 index 0000000..5ffef05 --- /dev/null +++ b/build/scripts-3.6/pylabsetup.py @@ -0,0 +1,41 @@ +import matplotlib as mpl +import pylab as plt + +mpl.rcParams.update(mpl.rcParamsDefault) +mpl.rcParams['font.size'] = 26. +mpl.rcParams['font.family'] = 'serif' +#mpl.rcParams['font.family'] = 'serif' +mpl.rcParams['font.serif'] = ['Times New Roman', 'Times', 'Palatino', 'Charter', 'serif'] +mpl.rcParams['font.sans-serif'] = ['Helvetica'] +mpl.rcParams['axes.labelsize'] = 24 +mpl.rcParams['xtick.labelsize'] = 22. +mpl.rcParams['ytick.labelsize'] = 22. +mpl.rcParams['xtick.major.size'] = 15. +mpl.rcParams['xtick.minor.size'] = 10. +mpl.rcParams['ytick.major.size'] = 15. +mpl.rcParams['ytick.minor.size'] = 10. +#mpl.rcParams['figure.autolayout']= True + +#fontsize=26 +#mpl.rc('axes', titlesize=fontsize) +#mpl.rc('axes', labelsize=fontsize) +#mpl.rc('xtick', labelsize=fontsize) +#mpl.rc('ytick', labelsize=fontsize) +#mpl.rc('font', size=fontsize, family='serif', serif='Utopia', +# style='normal', variant='normal', +# stretch='normal', weight='normal') +#mpl.rc('font',**{'family':'serif','serif':[ 'Times New Roman', 'Times', 'serif'], +# 'sans-serif':['Helvetica'], 'size':19, +# 'weight':'normal'}) +mpl.rc('axes', **{'linewidth' : 1.2}) +mpl.rc('axes', **{'labelweight': 'normal', 'linewidth': 1}) +mpl.rc('axes', **{'labelweight': 'normal', 'linewidth': 1}) +mpl.rc('ytick', **{'major.pad': 5, 'color': 'k'}) +mpl.rc('xtick', **{'major.pad': 5, 'color': 'k'}) +params = {'legend.fontsize': 24, + 'legend.numpoints': 1, + 'legend.handletextpad': 1 + } + +plt.rcParams.update(params) +plt.minorticks_on() diff --git a/pyMCZ/mcz.py b/pyMCZ/mcz.py index db024ec..52df3f5 100644 --- a/pyMCZ/mcz.py +++ b/pyMCZ/mcz.py @@ -35,14 +35,14 @@ # Define the version of __version__ = '1.3.1' -NM0 = 0 # setting this to say N>0 starts the calculation at measurement N. +NM0 = 0 # setting this to say N>0 starts the calculation at measurement N. #this is only for exploratory purposes as the code bugs out before #plotting and printing the results PROFILING = True PROFILING = False -alllines = ['[OII]3727', 'Hb', '[OIII]4959', '[OIII]5007', '[OI]6300', 'Ha', '[NII]6584', '[SII]6717', '[SII]6731', '[SIII]9069', '[SIII]9532'] +alllines = ['[OII]3727', 'Hg','Hb', '[OIII]4959', '[OIII]5007', '[OI]6300', 'Ha', '[NII]6584', '[SII]6717', '[SII]6731', '[SIII]9069', '[SIII]9532'] morelines = ['E(B-V)', 'dE(B-V)', 'scale_blue', 'd scale_blue'] MAXPROCESSES = 10 @@ -130,7 +130,12 @@ def readfile(filename): l0 = f.readline().replace(' ', '') l1 = f.readline().split() if l0.startswith('#') or l0.startswith(';'): - header = l0.strip().replace(";", '').replace("#", '').split(',') + # Modified to account for non-standard header (\t instead of , separated) jch + temp_header = l0.strip().replace(";", '').replace("#", '') + if temp_header.find(',') == -1: + header = temp_header.split('\t') + else: + header = temp_header.split(',') header[0] = header[0].replace(' ', '') header = header[:len(l1)] else: @@ -146,7 +151,7 @@ def readfile(filename): newheader.append(h) header = newheader - + formats = ['S10'] + ['f'] * (len(header) - 1) if 'flag' in header: @@ -183,8 +188,8 @@ def ingest_data(filename, path): ,1:].shape + (-1, ))[: ,1:]) if snr[~np.isnan(snr)].any() < 3: - raw_input('''WARNING: signal to noise ratio smaller than 3 - for at least some lines! You should only use SNR>3 + raw_input('''WARNING: signal to noise ratio smaller than 3 + for at least some lines! You should only use SNR>3 measurements (return to proceed)''') except (IndexError, TypeError): pass @@ -314,8 +319,8 @@ def savehist(data, snname, Zs, nsample, i, path, nmeas, measnames, verbose=False try: from sklearn.neighbors import KernelDensity except ImportError: - print ('''sklearn is not available, - thus we cannot compute kernel density. + print ('''sklearn is not available, + thus we cannot compute kernel density. switching to bayesian blocks''') BINMODE = 'bb' if BINMODE == 'kd': @@ -429,7 +434,7 @@ def savehist(data, snname, Zs, nsample, i, path, nmeas, measnames, verbose=False return "-1\t -1\t -1", [], None -def calc((i, (sample, flux, err, nm, bss, mds, disp, dust_corr, +def calc((i, (sample, flux, err, nm, bss, mds, disp, dust_corr,dust_blue, verbose, res, scales, nps, logf))): logf = sys.stdout logf.write("\n\nreading in measurements %d\n"%(i + 1)) @@ -441,7 +446,9 @@ def calc((i, (sample, flux, err, nm, bss, mds, disp, dust_corr, #print >> logf, '{0:0.2} +/- {1:0.2}'.format(flux[k][i], err[k][i]) fluxi[k] = flux[k][i] * np.ones(len(sample[i])) + err[k][i] * sample[i] warnings.filterwarnings("ignore") - success = metallicity.calculation(scales[i], fluxi, nm, mds, nps, logf, disp=disp, dust_corr=dust_corr, verbose=verbose) + success = metallicity.calculation(scales[i], fluxi, nm, mds, nps, logf, + disp=disp, verbose=verbose, + dust_corr=dust_corr,dust_blue=dust_blue) if success == -1: logf.write("MINIMUM REQUIRED LINES: '[OII]3727','[OIII]5007','[NII]6584','[SII]6717'\n") #print >> logf, "MINIMUM REQUIRED LINES: '[OII]3727','[OIII]5007','[NII]6584','[SII]6717'" @@ -467,7 +474,8 @@ def calc((i, (sample, flux, err, nm, bss, mds, disp, dust_corr, ## mode 't' calculates this based on 2*n**1/3 ############################################################################## #@profile -def run((name, flux, err, nm, path, bss), nsample, mds, multiproc, logf, unpickle=False, dust_corr=True, verbose=False, fs=24): +def run((name, flux, err, nm, path, bss), nsample, mds, multiproc, logf, + unpickle=False, dust_corr=True, dust_blue=False, verbose=False, fs=24): global RUNSIM # ,BINMODE#,NOPLOT assert(len(flux[0]) == len(err[0])), "flux and err must be same dimensions" assert(len(flux['galnum']) == nm), "flux and err must be of declaired size" @@ -503,7 +511,7 @@ def run((name, flux, err, nm, path, bss), nsample, mds, multiproc, logf, unpickl raw_input("missing pickled file for this simulation: name, nsample.\nrun the MonteCarlo? Ctr-C to exit, Return to continue?\n") RUNSIM = True else: - pklfile = open(picklefile, 'rb') + pklfile = open(picklefile, 'rb') res = pickle.load(pklfile) if RUNSIM: @@ -517,7 +525,7 @@ def run((name, flux, err, nm, path, bss), nsample, mds, multiproc, logf, unpickl sample = [errordistrib(dargs, newnsample) for i in range(NM0, nm)] if sample == -1: return -1 - + ###Start calculation### ## the flux to be feed to the calculation will be ## flux + error*i @@ -549,7 +557,7 @@ def run((name, flux, err, nm, path, bss), nsample, mds, multiproc, logf, unpickl logf.write( "\n\n\nrunning on %d threads\n\n\n" % nps) #print >> logf, "\n\n\nrunning on %d threads\n\n\n" % nps - second_args = [sample, flux, err, nm, bss, mds, VERBOSE, dust_corr, VERBOSE, res, scales, nps, logf] + second_args = [sample, flux, err, nm, bss, mds, VERBOSE, dust_corr, dust_blue, VERBOSE, res, scales, nps, logf] pool = mpc.Pool(processes=nps) # depends on available cores rr = pool.map(calc, itertools.izip(range(NM0, nm), itertools.repeat(second_args))) # for i in range(nm): result[i] = f(i, second_args) @@ -591,7 +599,9 @@ def run((name, flux, err, nm, path, bss), nsample, mds, multiproc, logf, unpickl logf.write("\n") #print >> logf, "" - success = metallicity.calculation(scales, fluxi, nm, mds, 1, logf, disp=VERBOSE, dust_corr=dust_corr, verbose=VERBOSE) + success = metallicity.calculation(scales, fluxi, nm, mds, 1, logf, + disp=VERBOSE, verbose=VERBOSE, + dust_corr=dust_corr, dust_blue=dust_blue) if success == -1: print ("MINIMUM REQUIRED LINES: [OII]3727 & [OIII]5007, or [NII]6584, and Ha & Hb if you want dereddening") #continue @@ -713,22 +723,23 @@ def main(): parser.add_argument('name', metavar='', type=str, help="the SN file name (root of the _min,_max file names") parser.add_argument('nsample', metavar='N', type=int, help="number of iterations, minimum 100 (or 1 for no MC sampling)") parser.add_argument('--clobber', default=False, action='store_true', help="replace existing output") - parser.add_argument('--binmode', default='k', type=str, choices=['d', 's', 'k', 't', 'bb', 'kd'], help='''method to determine bin size - {d: Duanes formula, s: n^1/2, t: 2*n**1/3(default), k: Knuth's rule, + parser.add_argument('--binmode', default='k', type=str, choices=['d', 's', 'k', 't', 'bb', 'kd'], help='''method to determine bin size + {d: Duanes formula, s: n^1/2, t: 2*n**1/3(default), k: Knuth's rule, bb: Bayesian blocks, kd: Kernel Density}''') - parser.add_argument('--path', default=None, type=str, help='''input/output path (must contain the input _max.txt and + parser.add_argument('--path', default=None, type=str, help='''input/output path (must contain the input _max.txt and _min.txt files in a subdirectory sn_data)''') parser.add_argument('--unpickle', default=False, action='store_true', help="read the pickled realization instead of making a new one") parser.add_argument('--verbose', default=False, action='store_true', help="verbose mode") parser.add_argument('--log', default=None, type=str, help="log file, if not passed defaults to standard output") parser.add_argument('--nodust', default=False, action='store_true', help=" don't do dust corrections (default is to do it)") + parser.add_argument('--bluedust', default=False, action='store_true', help=" use Hg/Hb for dust correction (default is Hb/Ha)") parser.add_argument('--noplot', default=False, action='store_true', help=" don't plot individual distributions (default is to plot all distributions)") parser.add_argument('--asciiout', default=False, action='store_true', help=" write distribution in an ascii output (default is not to)") parser.add_argument('--asciidistrib', default=False, action='store_true', help=" write distribution in an ascii output (default is not to)") - parser.add_argument('--md', default='all', type=str, help='''metallicity diagnostic to calculate. - default is 'all', options are: - D02, Z94, M91, C01, P05, M08, M08all, M13, PP04, D13, D13all, KD02, DP00 (deprecated), P01, D16''') + parser.add_argument('--md', default='all', type=str, help='''metallicity diagnostic to calculate. + default is 'all', options are: + D02, Z94, M91, C01, P05, M08, M08all, M13, PP04, D13, D13all, KD02, DP00 (deprecated), P01, D16, PG16, C17''') parser.add_argument('--multiproc', default=False, action='store_true', help=" multiprocess, with number of threads max(available cores-1, MAXPROCESSES)") args = parser.parse_args() @@ -752,8 +763,8 @@ def main(): if args.path: path = args.path else: - assert (os.getenv("MCMetdata")), ''' the _max, _min (and _med) data must live in a folder named sn_data. - pass a path to the sn_data folder, or set up the environmental variable + assert (os.getenv("MCMetdata")), ''' the _max, _min (and _med) data must live in a folder named sn_data. + pass a path to the sn_data folder, or set up the environmental variable MCMetdata pointing to the path where sn_data lives ''' path = os.getenv("MCMetdata") assert(os.path.isdir(path)), "pass a path or set up the environmental variable MCMetdata pointing to the path where the _min _max _med files live" @@ -763,7 +774,10 @@ def main(): fi = input_data(args.name, path=path) if fi != -1: logf = smart_open(args.log) - run(fi, args.nsample, args.md, args.multiproc, logf, unpickle=args.unpickle, dust_corr=(not args.nodust), verbose=VERBOSE) + run(fi, args.nsample, args.md, args.multiproc, logf, + unpickle=args.unpickle, + dust_corr=(not args.nodust), dust_blue=(args.bluedust), + verbose=VERBOSE) if args.log: logf.close() else: diff --git a/pyMCZ/metallicity.py b/pyMCZ/metallicity.py index d2b0f4a..d5d2e79 100644 --- a/pyMCZ/metallicity.py +++ b/pyMCZ/metallicity.py @@ -1,6 +1,6 @@ ############################################################################## -## Calculates oxygen abundance (here called metalicity) based on strong emission lines, -## based on code originally written in IDL by Lisa Kewley (Kewley & Ellison 2008). Outputs +## Calculates oxygen abundance (here called metalicity) based on strong emission lines, +## based on code originally written in IDL by Lisa Kewley (Kewley & Ellison 2008). Outputs ## oxygen abundance in many different diagnostics (see Bianco et al. 2016). ## ##new calculation based on the most recent version of the .pro file. @@ -24,9 +24,11 @@ DROPNEGATIVES = False # set to true if no negative flux measurements should be allowed. all negative flux measurements are dropped if DROPNEGATIVES: FIXNEGATIVES = False - + ##list of metallicity methods, in order calculated Zs = ["E(B-V)", # based on Halpha, Hbeta + "E(B-V)blue", # based on Hbeta, Hgamma + "C(Hbeta)", "logR23", # Hbeta, [OII]3727, [OIII]5007, [OIII]4959 "D02", # Halpha, [NII]6584 @@ -42,7 +44,7 @@ "DP00", # S23 available but deprecated "P10_ONS", "P10_ON", "M08_R23", "M08_N2Ha", "M08_O3Hb", "M08_O2Hb", "M08_O3O2", "M08_O3N2", - "M13_O3N2", "M13_N2", + "M13_O3N2", "M13_N2Ha", "D13_N2S2_O3S2", "D13_N2S2_O3Hb", "D13_N2S2_O3O2", "D13_N2O2_O3S2", "D13_N2O2_O3Hb", "D13_N2O2_O3O2", @@ -53,8 +55,12 @@ "KK04_R23", # Hbeta, [OII]3727, [OIII]5007, ([OIII]4959 ) "KD02comb", "PM14", - "D16"] # ,"KK04comb"] -#'KD02_N2O2', 'KD03new_R23', 'M91', 'KD03_N2Ha' + "PMC09_N2Ha", + "B07_N2O2", # Bresolin (2007) N2O2 + "D16", # Dopita (2016) ONS + "PG16_R","PG16_S", # Pilyugin & Grebel (2016) + "C17_O3O2", "C17_O3N2","C17_N2Ha","C17_O2Hb","C17_O3Hb","C17_R23"] + Zserr = ['PM14err'] # ,"KK04comb"] #'KD02_N2O2', 'KD03new_R23', 'M91', 'KD03_N2Ha' @@ -79,17 +85,19 @@ def printsafemulti(string, logf, nps): ############################################################################## -##fz_roots function as used in the IDL code +##fz_roots function as used in the IDL code ############################################################################## #@profile -def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, disp=False, verbose=False): +def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, + dust_blue=False, disp=False, verbose=False): global IGNOREDUST mscales.setdustcorrect() raw_lines = {} raw_lines['[OIII]5007'] = np.array([float('NaN')]) raw_lines['Hb'] = np.array([float('NaN')]) + raw_lines['Hg'] = np.array([float('NaN')]) raw_lines['Hz'] = np.array([float('NaN')]) for k in measured.iterkeys(): #kills all non-finite terms @@ -107,20 +115,24 @@ def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, disp=Fal ######change this only if youre spectra are very high SNR raw_lines['[OIII]4959'] = raw_lines['[OIII]5007'] / 3. raw_lines['[OIII]49595007'] = raw_lines['[OIII]4959'] + raw_lines['[OIII]5007'] - mscales.setHab(raw_lines['Ha'], raw_lines['Hb']) + mscales.setHabg(raw_lines['Ha'], raw_lines['Hb'], raw_lines['Hg']) #if Ha or Hb is zero, cannot do red correction - if dust_corr and mscales.hasHa and mscales.hasHb: + if dust_corr and mscales.hasHa and mscales.hasHb and not dust_blue: with np.errstate(invalid='ignore'): mscales.calcEB_V() + elif dust_blue and mscales.hasHg and mscales.hasHb: + with np.errstate(invalid='ignore'): + mscales.calcEB_Vblue() + print('----- Using Hg/Hb for E(B-V)_blue estimate. -----') elif dust_corr and not IGNOREDUST: if nps > 1: - print ('''WARNING: reddening correction cannot be done + print ('''WARNING: reddening correction cannot be done without both H_alpha and H_beta measurement!!''') else: - response = raw_input('''WARNING: reddening correction cannot be done without both H_alpha and H_beta measurement!! + response = raw_input('''WARNING: reddening correction cannot be done without both H_alpha and H_beta measurement!! Continuing without reddening correction? [Y/n]\n''').lower() assert(not (response.startswith('n'))), "please fix the input file to include Ha and Hb measurements" @@ -166,7 +178,7 @@ def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, disp=Fal sys.path.insert(0, cmd_folder) mscales.calcpyqz() else: - printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: set path to pyqz as environmental variable : export PYQZ_DIR="your/path/where/pyqz/resides/ in bash, for example, if you want this scale. \n''', logf, nps) @@ -176,7 +188,7 @@ def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, disp=Fal mscales.calcPP04() #mscales.calcP05() - mscales.calcP10() + #mscales.calcP10() mscales.calcM08() mscales.calcM13() @@ -187,6 +199,10 @@ def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, disp=Fal mscales.calcKK04_R23() mscales.calcKDcombined() + mscales.calcD16() + mscales.calcPG16() + mscales.calcC17() + if 'DP00' in mds: mscales.calcDP00() if 'P01' in mds: @@ -205,8 +221,8 @@ def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, disp=Fal #using the commented line below instead mscales.calcpyqz(plot=disp) else: - printsafemulti('''WARNING: CANNOT CALCULATE pyqz: - set path to pyqz as environmental variable + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable PYQZ_DIR if you want this scale. ''', logf, nps) if 'D13all' in mds: @@ -220,8 +236,8 @@ def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, disp=Fal #using the commented line below instead mscales.calcpyqz(plot=disp, allD13=True) else: - printsafemulti('''WARNING: CANNOT CALCULATE pyqz: - set path to pyqz as environmental variable + printsafemulti('''WARNING: CANNOT CALCULATE pyqz: + set path to pyqz as environmental variable PYQZ_DIR if you want this scale. ''', logf, nps) if 'PM14' in mds: @@ -253,5 +269,29 @@ def calculation(mscales, measured, num, mds, nps, logf, dust_corr=True, disp=Fal mscales.calcKK04_N2Ha() mscales.calcKK04_R23() mscales.calcKDcombined() + + # Modifications by jch [5/2017, Santiago] to include newer scales. + if 'B07_N2O2' in mds: + # Bresolin 2007 N2O2 + mscales.calcB07() if 'D16' in mds: + # Dopita+ 2016 ONS mscales.calcD16() + if 'PG16' in mds: + # Pilyugin & Grebel 2016 + mscales.calcPG16() + if 'C17' in mds: + # Curti+ 2017 + mscales.calcC17() + if 'C17all' in mds: + # Curti+ 2017 + mscales.calcC17(allC17=True) + if 'PMC09_N2Ha' in mds: + # Perez-Montero & Contini (2009) + mscales.calcPMC09() + + if 'KK04_N2Ha' in mds: + mscales.calcKD02_N2O2() + mscales.calcKK04_N2Ha() + elif 'KD02_N2O2' in mds: + mscales.calcKD02_N2O2() diff --git a/pyMCZ/metscales.py b/pyMCZ/metscales.py index 04e2e2d..607ac6e 100644 --- a/pyMCZ/metscales.py +++ b/pyMCZ/metscales.py @@ -5,10 +5,13 @@ import numpy.polynomial.polynomial as nppoly from metallicity import get_keys, printsafemulti -niter = 5 # number of iteations+1 for KD02 methods +import pdb + +niter = 5 # number of iterations+1 for KD02 methods k_Ha = 2.535 # CCM Rv=3.1 k_Hb = 3.609 # CCM Rv=3.1 +k_Hg = 4.400 # CCM Rv=3.1 #k_O1=2.661 # CCM Rv=3.1 k_O2 = 4.771 # CCM Rv=3.1 @@ -48,7 +51,7 @@ O3O2_coef=np.zeros((4,8)) # coefficients from model grid fits O3O2c0=[-36.9772,-74.2814,-36.7948,-81.1880,-52.6367,-86.8674,-24.4044,49.4728] -O3O2_coef[:,0]=[-36.9772,10.2838,-0.957421,0.0328614] #z=0.05 +O3O2_coef[:,0]=[-36.9772,10.2838,-0.957421,0.0328614] #z=0.05 O3O2_coef[:,1]=[-74.2814,24.6206,-2.79194,0.110773] # z=0.1 O3O2_coef[:,2]=[-36.7948,10.0581,-0.914212,0.0300472] # z=0.2 O3O2_coef[:,3]=[-81.1880,27.5082,-3.19126,0.128252] # z=0.5 @@ -65,6 +68,13 @@ 'O2Hb': [0.5603, 0.0450, -1.8017, -1.8434, -0.6549], 'O3N2': [0.4520, -2.6096, -0.7170, 0.1347]} +C17_coefs = {'N2Ha': [-0.489, 1.513,-2.554,-5.293,-2.867], + 'O3O2': [-0.691,-2.944,-1.308], + 'O3N2': [0.281,-4.765,-2.268], + 'O2Hb': [0.418,-0.961,-3.505,-1.949], + 'O3Hb': [-0.277,-3.549,-3.593,-0.981], + 'R23': [0.527,-1.569,-1.652,-0.421]} + #this is to check the Maiolino coefficients and find the split maximum of the cirves with degeneracy ''' import pylab as pl @@ -88,8 +98,9 @@ def __init__(self, num, logf, nps): self.nm = num self.Ha = None self.Hb = None + self.Hg = None - self.hasHa, self.hasHb = False, False + self.hasHa, self.hasHb, self.hasHg = False, False, False self.hasO2, self.hasO3 = False, False self.hasS2, self.hasN2 = False, False @@ -229,7 +240,7 @@ def printme(self, verbose=False): except (IndexError, TypeError): if verbose: print (self.mds[k]) - + def checkminimumreq(self, red_corr, ignoredust): if red_corr and not ignoredust: if not self.hasHa: @@ -281,13 +292,16 @@ def dustcorrect(self, l1, l2, flux=False): return 0 return 1.0 - def setHab(self, Ha, Hb): + def setHabg(self, Ha, Hb, Hg): self.Ha = Ha self.Hb = Hb + self.Hg = Hg if sum(self.Ha > 0): self.hasHa = True if sum(self.Hb > 0): self.hasHb = True + if sum(self.Hg > 0): + self.hasHg = True def setOlines(self, O23727, O35007, O16300, O34959): self.O23727 = O23727 @@ -322,7 +336,7 @@ def setOlines(self, O23727, O35007, O16300, O34959): if self.hasO2: self.logO2Hb = np.log10(self.O23727 / self.Hb) + \ self.dustcorrect(k_O2, k_Hb, flux=False) - # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) if self.hasO3: self.O3Hb = (self.O35007 / self.Hb) * \ self.dustcorrect(k_O35007, k_Hb, flux=True) @@ -347,13 +361,13 @@ def setNII(self, N26584): self.N26584 = N26584 self.hasN2 = True if self.hasHa: - self.logN2Ha = np.log10(self.N26584 / self.Ha) # +self.dustcorrect(k_N2,k_Ha,flux=True) + self.logN2Ha = np.log10(self.N26584 / self.Ha) # +self.dustcorrect(k_N2,k_Ha,flux=True) #lines are very close: no dust correction #Note: no dust correction cause the lies are really close! else: printsafemulti("WARNING: needs NII6584 and Ha to calculate NIIHa: did you run setHab()?", self.logf, self.nps) if self.hasS2 and self.hasS26731 and self.hasN2: - self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_S2,flux=True) + self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_S2,flux=True) #lines are very close: no dust correction if self.hasO2 and self.hasN2: self.NII_OII = np.log10(self.N26584 / self.O23727 * self.dustcorrect(k_N2, k_O2, flux=True)) @@ -378,7 +392,7 @@ def setSII(self, S26717, S26731, S39069, S39532): self.hasS39532 = True if self.hasS2: if self.hasN2 and self.NII_SII is None and self.hasS26731: - self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_O2,flux=True) + self.NII_SII = np.log10(self.N26584 / (self.S26717 + self.S26731)) # +self.dustcorrect(k_N2,k_O2,flux=True) #lines are very close: no dust correction if self.hasO3 and self.OIII_SII is None and self.hasS26731: self.OIII_SII = np.log10(self.O35007 / (self.S26717 + self.S26731) * self.dustcorrect(k_O3, k_S2, flux=True)) @@ -386,7 +400,15 @@ def setSII(self, S26717, S26731, S39069, S39532): #@profile def calcEB_V(self): printsafemulti("calculating E(B-V)", self.logf, self.nps) - self.mds['E(B-V)'] = np.log10(2.86 * self.Hb / self.Ha) / (0.4 * (k_Ha - k_Hb)) # E(B-V) + a2b_intrinsic = 2.86 + self.mds['E(B-V)'] = np.log10(a2b_intrinsic * self.Hb / self.Ha) / (0.4 * (k_Ha - k_Hb)) # E(B-V) + self.mds['E(B-V)'][self.mds['E(B-V)'] <= 0] = 1e-5 + + #@profile + def calcEB_Vblue(self): + printsafemulti("calculating E(B-V)_blue", self.logf, self.nps) + b2g_intrinsic = 2.145 + self.mds['E(B-V)'] = np.log10(b2g_intrinsic * self.Hg / self.Hb) / (0.4 * (k_Hb - k_Hg)) # E(B-V) self.mds['E(B-V)'][self.mds['E(B-V)'] <= 0] = 1e-5 #@profile @@ -402,15 +424,16 @@ def calcNIISII(self): def calcNIIOII(self): if self.hasN2 and self.hasO2: self.logN2O2 = np.log10(self.N26584 / self.O23727) + self.dustcorrect(k_N2, k_O2, flux=False) + self.hasN2O2 = True if not self.hasN2O2 or np.mean(self.logN2O2) < -1.2: try: - printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods should only be used for log([NII]6564/[OII]3727) > -1.2, + printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods should only be used for log([NII]6564/[OII]3727) > -1.2, the mean log([NII]6564/[OII]3727)= %f''' % np.mean(self.logN2O2), self.logf, self.nps) except TypeError: - printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods - should only be used for log([NII]6564/[OII]3727) >-1.2, + printsafemulti('''WARNING: the KD02 and KK04 (+M08) methods + should only be used for log([NII]6564/[OII]3727) >-1.2, the mean log([NII]6564/[OII]3727)= %s''' % self.logN2O2, self.logf, self.nps) if not self.hasN2O2: @@ -529,11 +552,10 @@ def calcpyqz(self, plot=False, allD13=False): np.atleast_1d([self.OIII_SII])], \ '[NII]/[SII]+;[OIII]/[SII]+')[0].T else: - self.mds['D13_N2S2_O3S2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ - np.atleast_1d([self.OIII_SII])], \ - '[NII]/[SII]+;[OIII]/[SII]+', \ - show_plot=plot, n_plot=False, \ - save_plot=False, verbose=False)[0].T + self.mds['D13_N2S2_O3S2'] = pyqz.interp_qz('Tot[O]+12', + [np.atleast_1d([self.NII_SII]), + np.atleast_1d([self.OIII_SII])], + '[NII]/[SII]+;[OIII]/[SII]+')[0].T if self.OIII_Hb is not None: if oldpyqz: @@ -547,11 +569,14 @@ def calcpyqz(self, plot=False, allD13=False): else: + # self.mds['D13_N2S2_O3SHb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ + # np.atleast_1d([self.OIII_Hb])], \ + # '[NII]/[SII]+;[OIII]/Hb', \ + # show_plot=plot, n_plot=False, \ + # save_plot=False, verbose=False)[0].T self.mds['D13_N2S2_O3SHb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_SII]), \ np.atleast_1d([self.OIII_Hb])], \ - '[NII]/[SII]+;[OIII]/Hb', \ - show_plot=plot, n_plot=False, \ - save_plot=False, verbose=False)[0].T + '[NII]/[SII]+;[OIII]/Hb')[0].T @@ -571,7 +596,7 @@ def calcpyqz(self, plot=False, allD13=False): show_plot=plot, n_plot=False, \ save_plot=False, verbose=False)[0].T - + if self.NII_OII is not None and allD13: if self.OIII_SII is not None: if oldpyqz: @@ -602,9 +627,7 @@ def calcpyqz(self, plot=False, allD13=False): else: self.mds['D13_N2O2_O3Hb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ np.atleast_1d([self.OIII_Hb])], \ - '[NII]/[OII]+;[OIII]/Hb', \ - show_plot=plot, n_plot=False, \ - save_plot=False, verbose=False)[0].T + '[NII]/[OII]+;[OIII]/Hb')[0].T if self.OIII_OII is not None: if oldpyqz: @@ -618,9 +641,7 @@ def calcpyqz(self, plot=False, allD13=False): else: self.mds['D13_N2O2_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.NII_OII]), \ np.atleast_1d([self.OIII_OII])], \ - '[NII]/[OII]+;[OIII]/[OII]+', \ - show_plot=plot, n_plot=False, \ - save_plot=False, verbose=False)[0].T + '[NII]/[OII]+;[OIII]/[OII]+')[0].T if self.logN2Ha is not None: if self.OIII_Hb is not None: if oldpyqz: @@ -634,9 +655,7 @@ def calcpyqz(self, plot=False, allD13=False): else: self.mds['D13_N2Ha_O3Hb'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.logN2Ha]), \ np.atleast_1d([self.OIII_Hb])], \ - '[NII]/Ha;[OIII]/Hb', \ - show_plot=plot, n_plot=False, \ - save_plot=False, verbose=False)[0].T + '[NII]/Ha;[OIII]/Hb')[0].T if self.OIII_OII is not None: if oldpyqz: self.mds['D13_N2Ha_O3O2'] = pyqz.get_qz(20, 'z', np.atleast_1d([self.logN2Ha]), \ @@ -650,9 +669,7 @@ def calcpyqz(self, plot=False, allD13=False): else: self.mds['D13_N2Ha_O3O2'] = pyqz.interp_qz('Tot[O]+12', [np.atleast_1d([self.logN2Ha]), \ np.atleast_1d([self.OIII_Hb])], \ - '[NII]/Ha;[OIII]/[OII]+', \ - show_plot=plot, n_plot=False, \ - save_plot=False, verbose=False)[0].T + '[NII]/Ha;[OIII]/[OII]+')[0].T #@profile def calcDP00(self): @@ -795,7 +812,7 @@ def calcP10(self): #independent on physical conditions #The Physics and Dynamics of Planetary Nebulae # By Grigor A. Gurzadyan - P10logN2 = (np.log((self.N26584 * 1.33) / self.Hb) + P10logN2 = (np.log((self.N26584 * 1.33) / self.Hb) + self.dustcorrect(k_N2, k_Hb, flux=False)) if self.hasS2 and self.hasS26731: @@ -879,7 +896,7 @@ def calcC01_ZR23(self): self.mds['C01_R23'][self.O2O35007 >= 0.8] = np.log10(3.96e-4 * x3[self.O2O35007 >= 0.8] ** (-0.46)) + 12.0 else: - printsafemulti('''WARNING: need [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, + printsafemulti('''WARNING: need [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, did you set them up with setOlines()?''', self.logf, self.nps) # Charlot 01 calibration: (case A) based on [N2]/[SII]## @@ -890,7 +907,7 @@ def calcC01_ZR23(self): if self.hasN2S2 and self.hasO3 and self.hasO2 and self.hasO3Hb: self.mds['C01_N2S2'] = np.log10(5.09e-4 * (x2 ** 0.17) * ((self.N2S2 / 0.85) ** 1.17)) + 12 else: - printsafemulti('''WARNING: needs [NII]6584, [SII]6717, [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, + printsafemulti('''WARNING: needs [NII]6584, [SII]6717, [OIII]5700, [OII]3727, and Ha to calculate calcC01_ZR23, did you set them up with setOlines() and ?''', self.logf, self.nps) #@profile @@ -941,30 +958,30 @@ def calcM13(self): printsafemulti("calculating M13", self.logf, self.nps) if not self.hasHa or not self.hasN2: - printsafemulti("WARNING: need O3, N2, Ha and Hb, " + + printsafemulti("WARNING: need O3, N2, Ha and Hb, " + "or at least N2 and Ha", self.logf, self.nps) return -1 else: e1 = np.random.normal(0, 0.027, self.nm) e2 = np.random.normal(0, 0.024, self.nm) - self.mds["M13_N2"] = 8.743 + e1 + (0.462 + e2) * self.logN2Ha + self.mds["M13_N2Ha"] = 8.743 + e1 + (0.462 + e2) * self.logN2Ha if self.hasHb and self.hasO3: e1 = np.random.normal(0, 0.012, self.nm) e2 = np.random.normal(0, 0.012, self.nm) O3N2 = self.logO3Hb - self.logN2Ha self.mds["M13_O3N2"] = 8.533 + e1 - (0.214 + e1) * O3N2 - index = (O3N2 > 1.7) + index = (O3N2 > 1.7) self.mds["M13_O3N2"][index] = float('NaN') index = (O3N2 < -1.1) self.mds["M13_O3N2"][index] = float('NaN') - + #for i in range(len(self.mds["M13_O3N2"])): #print ("here O3N2",#self.O35007[i], self.Hb[i], - #self.O3Hb[i], + #self.O3Hb[i], #self.logO3Hb[i], self.logN2Ha[i], - # O3N2[i], + # O3N2[i], # self.mds["M13_O3N2"][i]) - + #@profile def calcM08(self, allM08=False): #Maiolino+ 2008 @@ -1117,7 +1134,7 @@ def calcKK04_N2Ha(self): convergence = np.abs(self.logq - logq_save).mean() logq_save = self.logq.copy() if ii >= 100: - printsafemulti("WARNING: loop did not converge", self.logf, self.nps) + printsafemulti("WARNING [KK04_N2Ha]: loop did not converge", self.logf, self.nps) Z_new_N2Ha = np.zeros(self.nm) + float('NaN') else: self.logq = 7.37177 * np.ones(self.nm) @@ -1177,7 +1194,7 @@ def calcKK04_R23(self): convergence = np.abs((logqold - logq).mean()) logqold = logq.copy() if ii >= 100: - printsafemulti("WARNING: loop did not converge", self.logf, self.nps) + printsafemulti("WARNING [KK04_R23]: loop did not converge", self.logf, self.nps) Z_new = np.zeros(self.nm) + float('NaN') Z_new_lims = [nppoly.polyval(self.logR23, [9.40, 4.65, -3.17]) - \ logq * nppoly.polyval(self.logR23, [0.272, 0.547, -0.513]), @@ -1296,7 +1313,7 @@ def calcPM14(self): ratios[2] = self.O3Hb elif self.hasO3: ratios[2] = ((self.O35007 / self.Hb) - * self.dustcorrect(k_O35007, k_Hb, flux=True)) # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) + * self.dustcorrect(k_O35007, k_Hb, flux=True)) # 0.4*self.mds['E(B-V)']*(k_O2-k_Hb) else: ratios[2] = np.zeros(self.nm) @@ -1313,7 +1330,7 @@ def calcPM14(self): else: ratios[4] = np.zeros(self.nm) - + for ni in range(self.nm): fin_hii_chi.write('%f %f %f %f %f\n' % (ratios[0][ni], ratios[1][ni], ratios[2][ni], ratios[3][ni], ratios[4][ni])) fin_hii_chi.close() @@ -1339,20 +1356,238 @@ def calcPM14(self): print (self.mds) os.system('rm -r C13*dat') + + def calcPMC09(self): + #Perez-Montero & Contini (2009) - N2Ha + # Added 5/2017 by jch, Santiago + printsafemulti("calculating PMC09", self.logf, self.nps) + + if not self.hasHa or not self.hasN2 or not self.hasHa: + printsafemulti("WARNING: need N2, Ha ", + self.logf, self.nps) + return -1 + PMC09_N2 = lambda x: 9.07+0.79*x + + y = 9.07 + 0.78*self.logN2Ha + self.mds["PMC09_N2Ha"] = y + + #@profile + def calcB07(self): + #Bresolin 2007 - N2O2 + # Added 5/2017 by jch, Santiago + printsafemulti("calculating B07", self.logf, self.nps) + + if not self.hasHa or not self.hasN2 or not self.hasO2: + printsafemulti("WARNING: need N2, O2 ", + self.logf, self.nps) + return -1 + + # Use [N2/Ha * Ha/Hb] / [O2/Hb] + x = (self.logN2Ha+np.log10(2.86)) - self.logO2Hb + + y = 8.66 + 0.36*self.logN2O2 - 0.17*self.logN2O2**2 + self.mds["B07_N2O2"] = y + index = (self.logN2O2 < -1.2) + self.mds["B07_N2O2"][index] = float('NaN') + index = (self.logN2O2 > 0.6) + self.mds["B07_N2O2"][index] = float('NaN') + + #@profile def calcD16(self): #Dopita+ 2016 + # Added 5/2017 by jch, Santiago printsafemulti("calculating D16", self.logf, self.nps) - + if not self.hasHa or not self.hasN2 or not self.hasS2: printsafemulti("WARNING: need N2, Ha and SII, ", self.logf, self.nps) return -1 - y = self.logN2S2 + 0.264 * self.logN2Ha - self.mds["D16"] = 8.77 + y - 0.45 * pow(y + 0.3, 5) + + d16_n2s2 = np.log10(self.N26584 / (self.S26717+self.S26731)) + self.dustcorrect(k_N2, k_S2, flux=False) + y = d16_n2s2 + 0.264 * self.logN2Ha + self.mds["D16"] = 8.77 + y# + 0.45 * pow(y + 0.3, 5) index = (y < -1.) - self.mds["D16"][index] = float('NaN') + self.mds["D16"][index] = float('NaN') index = (y > 0.5) - self.mds["D16"][index] = float('NaN') + self.mds["D16"][index] = float('NaN') + + + def calcC17(self, allC17=False): + # Curti+ 2017 + # Monthly Notices Royal Astronomical Society, vol. 465, pp 1384-1400 + # + # Added 5/2017 by jch, Santiago + import pdb + + valid_upper = 8.85 + valid_lower = 7.6 + + printsafemulti("calculating C17", self.logf, self.nps) + highZ = None + + if self.logO35007O2 is not None: + # C17 O3O2 + self.mds['C17_O3O2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O3O2']] * self.nm).T + coefs[0] = coefs[0] - self.logO35007O2 + + # sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + + # Find valid solutions + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + + # the two cumsum assure that if the condition for the ith element + # of indx is [False, False] then after the first cumsum(1) is [0,0] + # [False, True] is [0,1] + # [True, True] is [1,2] + # but (here is the kicker) [True, False] is [1,1]. + # Because i want only one solution + # (i'll settle for the first one occurring) [1,1] is ambiguous. + # The second cumsum(1) makes + # [0,0]->[0,0], [0,1]->[0,1], [1,2]->[1,3] and finally [1,1]->[1,2] - + # Set the results where appropriate + self.mds['C17_O3O2'][(indx.sum(1)) > 0] = sols[indx].real + + # Now use this to see if we're on the high-Z branch of R23 + highZ = np.median(self.logO35007O2) < 0 + + if self.logN2Ha is not None: + # C17 N2Ha + self.mds['C17_N2Ha'] = np.zeros(self.nm) + float('NaN') + + coefs = np.array([C17_coefs['N2Ha']] * self.nm).T + coefs[0] = coefs[0] - self.logN2Ha + + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + + self.mds['C17_N2Ha'][(indx.sum(1)) > 0] = sols[indx].real + if highZ is None: + highZ = np.median(self.logN2Ha) > -1.3 + + if self.hasO3 and self.hasN2: + # C17 O3N2 + self.mds['C17_O3N2'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O3N2']] * self.nm).T + o3n2_value = self.logO3Hb - self.logN2Ha + coefs[0] = coefs[0] - o3n2_value + + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) \ + * (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O3N2'][(indx.sum(1)) > 0] = sols[indx].real + + if self.logO3Hb is not None: + # C17 O3Hb + self.mds['C17_O3Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O3Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO3Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + + # C17 define this only for the high-metal branch. + indx = ((sols.real >= 8.2) * (sols.real <= valid_upper) * + (sols.imag == 0)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O3Hb'][(indx.sum(1)) > 0] = sols[indx].real + + # Require allC17 flag if we want everything. + if not allC17: + return + else: + printsafemulti("calculating other C17s", self.logf, self.nps) + + if self.logR23 is None: + printsafemulti("WARNING: Must first calculate R23", self.logf, self.nps) + self.calcR23() + if self.logR23 is None: + printsafemulti("WARNING: Cannot compute C17_R23 without R23", self.logf, self.nps) + else: + self.mds['C17_R23'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['R23']] * self.nm).T + coefs[0] = coefs[0] - self.logR23 + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0) * + (sols.real >= 8.4)).cumsum(1).cumsum(1) == 1 + self.mds['C17_R23'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= valid_lower) * (sols.real <= valid_upper) * + (sols.imag == 0) * + (sols.real < 8.4)).cumsum(1).cumsum(1) == 1 + self.mds['C17_R23'][(indx.sum(1)) > 0] = sols[indx].real + + + if self.logO2Hb is not None: + self.mds['C17_O2Hb'] = np.zeros(self.nm) + float('NaN') + coefs = np.array([C17_coefs['O2Hb']] * self.nm).T + coefs[0] = coefs[0] - self.logO2Hb + sols = np.array([self.fz_roots(coefs.T)])[0] + 8.69 + if highZ is True: + indx = ((sols.real >= valid_lower) * (sols.real <= 8.3) * + (sols.imag == 0) * + (sols.real >= 8.7)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + elif highZ is False: + indx = ((sols.real >= valid_lower) * (sols.real <= 8.3) * + (sols.imag == 0) * + (sols.real <= valid_upper)).cumsum(1).cumsum(1) == 1 + self.mds['C17_O2Hb'][(indx.sum(1)) > 0] = sols[indx].real + + + + def calcPG16(self): + # Pilyugin & Grebel (2016, MNRAS, 457, 3678) + # Added 5/2017 by jch, Santiago + printsafemulti("calculating PG16", self.logf, self.nps) + + # Initialize the PG16 results + self.mds['PG16'] = np.zeros(self.nm) + float('NaN') + + # Make sure the lines are all here... + if not self.hasHa or not self.hasN2 or not self.hasO2 or not self.hasO3 or not self.hasS2: + printsafemulti("WARNING: need O2, O3, N2, S2, and Hb", + self.logf, self.nps) + return -1 + + pilR2 = self.logO2Hb + pilR3 = self.logO3Hb+np.log10(1.33) # Both members of doublet + pilR3R2 = (pilR3-pilR2) + + # Calculate S2/Hb = S2/Ha * Ha/Hb = S2/Ha*2.85 + pilS2 = np.log10((self.S26731 + self.S26731) / self.Ha * 2.85) + pilS2 += np.log10(self.dustcorrect(k_S2,k_Ha,flux=True)) + pilR3S2 = (pilR3-pilS2) + + # Calculate N2/Hb = N2/Ha * Ha/Hb = N2/Ha*2.85 + pilN2 = np.log10((self.N26584*1.33 / self.Ha)*2.85) + # pilN2 += np.log10(self.dustcorrect(k_N2,k_Hb,flux=True)) + + # Determine which branch we're on + highZ = (np.median(pilN2) >= -0.6) + print(np.median(pilN2)) + + # Calculate R = O2O3N2 abundances + if highZ == True: + print('PG16 high metal branch') + self.mds['PG16_R'] = 8.589+0.022*pilR3R2+0.399*pilN2 + self.mds['PG16_R'] += (-0.137+0.164*pilR3R2+0.589*pilN2)*pilR2 + else: + print('PG16 low metal branch') + self.mds['PG16_R'] = 7.932+0.944*pilR3R2+0.695*pilN2 + self.mds['PG16_R'] += (0.970-0.291*pilR3R2-0.019*pilN2)*pilR2 + + # Calculate S = S2O3N2 abundances + if highZ == True: + print('PG16 high metal branch') + self.mds['PG16_S'] = 8.424+0.030*pilR3S2+0.751*pilN2 + self.mds['PG16_S'] += (-0.349+0.182*pilR3S2+0.508*pilN2)*pilS2 + else: + print('PG16 low metal branch') + self.mds['PG16_S'] = 8.072+0.789*pilR3S2+0.726*pilN2 + self.mds['PG16_S'] += (1.069-0.17*pilR3S2+0.022*pilN2)*pilS2