diff --git a/docs/Cluster_w_COSMIC.ipynb b/docs/Cluster_w_COSMIC.ipynb new file mode 100755 index 00000000..00314a16 --- /dev/null +++ b/docs/Cluster_w_COSMIC.ipynb @@ -0,0 +1,1544 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ff023bf0-605f-4d7c-ae4a-63ed04bfe5c1", + "metadata": {}, + "source": [ + "# SPISEA with COSMIC" + ] + }, + { + "cell_type": "markdown", + "id": "a003cd65-1a0a-48b6-9b24-f427f48bc651", + "metadata": {}, + "source": [ + "This is a quick tutorial to use SPISEA with COSMIC. COSMIC is a rapid binary synthesis code. We use it to evolve singles and binaries generated in a SPISEA cluster. In order to use this functionality, the user must install COSMIC. Info about installation and COSMIC usage can be found here: https://cosmic-popsynth.github.io/COSMIC/index.html\n", + "\n", + "Tutorial by Natasha Abrams 1-27-26" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "12bb2ed0-5349-485d-9bbf-eb3b2749743e", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "78a29e15-e5ca-4c8b-90b3-c72ba02ec8a0", + "metadata": {}, + "outputs": [], + "source": [ + "from spisea import synthetic, evolution, atmospheres, reddening, ifmr, filters\n", + "from spisea.imf import imf, multiplicity\n", + "import os\n", + "import matplotlib.pyplot as plt\n", + "from astropy.table import Table\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "26f38db3-4c0e-4e4e-8154-d34c10a4d099", + "metadata": {}, + "source": [ + "You can start by instantiating a SPISEA isochrone object as usual, with a few key differences:\n", + "- Evolution object is COSMIC. This takes the dictionary specifying the binary evolution parameters. For detailed instructions on changing the parameters in this dictionary see the [COSMIC docs](https://github.com/COSMIC-PopSynth/COSMIC/blob/v3.6.1/examples/Params.ini).\n", + "- You can also specify if you'd like to keep disrupted companions which will be added to the primary table or if you would like them to be dropped. We recommend keeping them and this is the default.\n", + "- We use the IsochronePhotExternalEvolution() object as the ischrone object. We name it this way for convention, but COSMIC does not use an Isochrone grid like the other evolution models in SPISEA. Instead it uses prescriptions from Hurley et al. 2002.\n", + "- Since no isochrones are used, we interpolate over every point in Teff, logg, and metallicity for the atmospehre grids. This takes longer than the typical generation of isocrhones in SPISEA, but will have to be done less frequently.\n", + "- We suggest having a separate directory than your typical isochrone directory where the atmosphere grids will be stored.\n", + "- We recommend using get_merged_atmsophere_w_bb_supplement since some objects generated by COSMIC are outside of supported SPISEA atmospheres. You can specify your own ranges to have black bodies defined or use our default ones." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "90c33d5c-7c52-4dd1-9545-25b00db26032", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/u/nsabrams/code/multiplicity/PyPopStar/spisea/atmospheres.py:1669: UserWarning: Only `temperature` keyword is used for black-body atmosphere\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "Changing to met=0.50 for met=0.30 T= 1100 logg=2.50\n", + "Changing to met=0.50 for met=0.30 T= 1100 logg=3.00\n", + "Changing to met=0.50 for met=0.30 T= 1100 logg=3.50\n", + "Changing to met=0.50 for met=0.30 T= 1100 logg=4.00\n", + "Changing to met=0.50 for met=0.30 T= 1100 logg=4.50\n", + "Changing to met=0.50 for met=0.30 T= 1100 logg=5.00\n", + "Changing to met=0.50 for met=0.30 T= 1100 logg=5.50\n", + "Changing to met=0.50 for met=0.30 T= 1200 logg=2.50\n", + "Changing to met=0.50 for met=0.30 T= 1200 logg=3.00\n", + "Changing to met=0.50 for met=0.30 T= 1200 logg=3.50\n", + "Changing to met=0.50 for met=0.30 T= 1200 logg=4.00\n", + "Changing to met=0.50 for met=0.30 T= 1200 logg=4.50\n", + "Changing to met=0.50 for met=0.30 T= 1200 logg=5.00\n", + "Changing to met=0.50 for met=0.30 T= 1200 logg=5.50\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "BB atmosphere\n", + "Atmosphere grid generation took 810.611348 s.\n", + "Making photometry for atmosphere grid: AKs = 0.00 dist = 4000\n", + " Starting at: 2026-06-10 22:10:31.613038 Usually takes ~5 minutes\n", + "Starting filter: ubv,U Elapsed time: 0.00 seconds\n", + "Starting synthetic photometry\n", + "M = 1000.000 Msun T = 2 K m_ubv_U = 44.31\n", + "M = 1000.000 Msun T = 4 K m_ubv_U = 44.31\n", + "M = 1000.000 Msun T = 4 K m_ubv_U = 44.31\n", + "M = 1000.000 Msun T = 6 K m_ubv_U = 44.31\n", + "M = 1400.000 Msun T = 4 K m_ubv_U = 36.26\n", + "M = 2600.000 Msun T = 3 K m_ubv_U = 26.83\n", + "M = 2600.000 Msun T = 4 K m_ubv_U = 27.90\n", + "M = 3400.000 Msun T = 2 K m_ubv_U = 25.51\n", + "M = 4100.000 Msun T = 6 K m_ubv_U = 21.99\n", + "M = 4900.000 Msun T = 4 K m_ubv_U = 19.67\n", + "M = 3000.000 Msun T = 2 K m_ubv_U = 26.78\n", + "M = 3800.000 Msun T = 0 K m_ubv_U = 24.84\n", + "M = 4500.000 Msun T = 4 K m_ubv_U = 20.73\n", + "M = 2600.000 Msun T = 2 K m_ubv_U = 28.57\n", + "M = 3400.000 Msun T = 0 K m_ubv_U = 25.71\n", + "M = 4100.000 Msun T = 5 K m_ubv_U = 21.94\n", + "M = 4900.000 Msun T = 3 K m_ubv_U = 19.64\n", + "M = 3000.000 Msun T = 1 K m_ubv_U = 26.41\n", + "M = 3700.000 Msun T = 6 K m_ubv_U = 23.58\n", + "M = 4500.000 Msun T = 4 K m_ubv_U = 20.69\n", + "M = 2600.000 Msun T = 2 K m_ubv_U = 27.43\n", + "M = 3300.000 Msun T = 6 K m_ubv_U = 25.94\n", + "M = 4100.000 Msun T = 4 K m_ubv_U = 22.11\n", + "M = 4900.000 Msun T = 2 K m_ubv_U = 19.99\n", + "M = 3000.000 Msun T = 0 K m_ubv_U = 27.51\n", + "M = 3700.000 Msun T = 4 K m_ubv_U = 23.36\n", + "M = 4500.000 Msun T = 2 K m_ubv_U = 21.24\n", + "M = 2900.000 Msun T = 2 K m_ubv_U = 26.08\n", + "M = 4100.000 Msun T = 6 K m_ubv_U = 22.37\n", + "M = 4900.000 Msun T = 4 K m_ubv_U = 20.42\n", + "M = 3000.000 Msun T = 2 K m_ubv_U = 26.58\n", + "M = 3700.000 Msun T = 6 K m_ubv_U = 23.41\n", + "M = 4500.000 Msun T = 4 K m_ubv_U = 21.75\n", + "M = 2600.000 Msun T = 2 K m_ubv_U = 27.09\n", + "M = 3400.000 Msun T = 0 K m_ubv_U = 27.04\n", + "M = 4100.000 Msun T = 4 K m_ubv_U = 22.80\n", + "M = 4900.000 Msun T = 2 K m_ubv_U = 21.51\n", + "M = 5000.000 Msun T = 2 K m_ubv_U = 19.43\n", + "M = 10500.000 Msun T = 3 K m_ubv_U = 15.28\n", + "M = 18000.000 Msun T = 5 K m_ubv_U = 13.73\n", + "M = 47000.000 Msun T = 4 K m_ubv_U = 11.39\n", + "M = 7750.000 Msun T = 1 K m_ubv_U = 16.58\n", + "M = 11000.000 Msun T = 2 K m_ubv_U = 15.06\n", + "M = 21000.000 Msun T = 3 K m_ubv_U = 13.18\n", + "M = 5500.000 Msun T = 2 K m_ubv_U = 18.80\n", + "M = 8000.000 Msun T = 2 K m_ubv_U = 16.52\n", + "M = 11250.000 Msun T = 4 K m_ubv_U = 15.20\n", + "M = 23000.000 Msun T = 4 K m_ubv_U = 13.08\n", + "M = 5750.000 Msun T = 3 K m_ubv_U = 18.34\n", + "M = 8250.000 Msun T = 4 K m_ubv_U = 16.44\n", + "M = 11750.000 Msun T = 4 K m_ubv_U = 15.00\n", + "M = 25000.000 Msun T = 5 K m_ubv_U = 12.92\n", + "M = 6000.000 Msun T = 4 K m_ubv_U = 18.00\n", + "M = 8750.000 Msun T = 2 K m_ubv_U = 16.02\n", + "M = 12250.000 Msun T = 4 K m_ubv_U = 14.87\n", + "M = 28000.000 Msun T = 4 K m_ubv_U = 12.59\n", + "M = 6250.000 Msun T = 4 K m_ubv_U = 17.70\n", + "M = 9000.000 Msun T = 4 K m_ubv_U = 16.08\n", + "M = 12750.000 Msun T = 4 K m_ubv_U = 14.57\n", + "M = 31000.000 Msun T = 4 K m_ubv_U = 12.15\n", + "M = 6750.000 Msun T = 0 K m_ubv_U = 17.54\n", + "M = 9500.000 Msun T = 3 K m_ubv_U = 15.66\n", + "M = 14000.000 Msun T = 3 K m_ubv_U = 14.20\n", + "M = 35000.000 Msun T = 4 K m_ubv_U = 11.81\n", + "M = 7000.000 Msun T = 2 K m_ubv_U = 17.34\n", + "M = 10000.000 Msun T = 2 K m_ubv_U = 15.29\n", + "M = 16000.000 Msun T = 3 K m_ubv_U = 13.75\n", + "M = 39000.000 Msun T = 4 K m_ubv_U = 11.60\n", + "M = 7250.000 Msun T = 2 K m_ubv_U = 17.16\n", + "M = 50000.000 Msun T = 8 K m_ubv_U = 11.28\n", + "M = 14000.000 Msun T = 8 K m_ubv_U = 14.38\n", + "M = 13750.000 Msun T = 8 K m_ubv_U = 14.44\n", + "M = 10750.000 Msun T = 7 K m_ubv_U = 15.32\n", + "M = 16500.000 Msun T = 8 K m_ubv_U = 13.90\n", + "M = 6750.000 Msun T = 9 K m_ubv_U = 17.40\n", + "M = 7000.000 Msun T = 7 K m_ubv_U = 17.18\n", + "M = 17250.000 Msun T = 10 K m_ubv_U = 13.78\n", + "M = 13250.000 Msun T = 8 K m_ubv_U = 14.56\n", + "M = 7750.000 Msun T = 8 K m_ubv_U = 16.67\n", + "M = 8500.000 Msun T = 7 K m_ubv_U = 16.26\n", + "M = 2601.716 Msun T = 7 K m_ubv_U = 27.24\n", + "M = 2601.716 Msun T = 8 K m_ubv_U = 27.24\n", + "M = 14736.126 Msun T = 10 K m_ubv_U = 13.75\n", + "M = 14736.126 Msun T = 11 K m_ubv_U = 13.75\n", + "Starting filter: ubv,V Elapsed time: 53.84 seconds\n", + "Starting synthetic photometry\n", + "M = 1000.000 Msun T = 2 K m_ubv_V = 40.30\n", + "M = 1000.000 Msun T = 4 K m_ubv_V = 40.30\n", + "M = 1000.000 Msun T = 4 K m_ubv_V = 40.30\n", + "M = 1000.000 Msun T = 6 K m_ubv_V = 40.30\n", + "M = 1400.000 Msun T = 4 K m_ubv_V = 32.80\n", + "M = 2600.000 Msun T = 3 K m_ubv_V = 26.27\n", + "M = 2600.000 Msun T = 4 K m_ubv_V = 22.75\n", + "M = 3400.000 Msun T = 2 K m_ubv_V = 22.10\n", + "M = 4100.000 Msun T = 6 K m_ubv_V = 19.88\n", + "M = 4900.000 Msun T = 4 K m_ubv_V = 18.94\n", + "M = 3000.000 Msun T = 2 K m_ubv_V = 22.76\n", + "M = 3800.000 Msun T = 0 K m_ubv_V = 21.43\n", + "M = 4500.000 Msun T = 4 K m_ubv_V = 19.46\n", + "M = 2600.000 Msun T = 2 K m_ubv_V = 24.08\n", + "M = 3400.000 Msun T = 0 K m_ubv_V = 22.06\n", + "M = 4100.000 Msun T = 5 K m_ubv_V = 20.04\n", + "M = 4900.000 Msun T = 3 K m_ubv_V = 18.84\n", + "M = 3000.000 Msun T = 1 K m_ubv_V = 23.21\n", + "M = 3700.000 Msun T = 6 K m_ubv_V = 20.82\n", + "M = 4500.000 Msun T = 4 K m_ubv_V = 19.36\n", + "M = 2600.000 Msun T = 2 K m_ubv_V = 25.31\n", + "M = 3300.000 Msun T = 6 K m_ubv_V = 21.87\n", + "M = 4100.000 Msun T = 4 K m_ubv_V = 20.07\n", + "M = 4900.000 Msun T = 2 K m_ubv_V = 18.85\n", + "M = 3000.000 Msun T = 0 K m_ubv_V = 25.66\n", + "M = 3700.000 Msun T = 4 K m_ubv_V = 20.88\n", + "M = 4500.000 Msun T = 2 K m_ubv_V = 19.38\n", + "M = 2900.000 Msun T = 2 K m_ubv_V = 25.37\n", + "M = 4100.000 Msun T = 6 K m_ubv_V = 20.11\n", + "M = 4900.000 Msun T = 4 K m_ubv_V = 18.79\n", + "M = 3000.000 Msun T = 2 K m_ubv_V = 25.65\n", + "M = 3700.000 Msun T = 6 K m_ubv_V = 21.12\n", + "M = 4500.000 Msun T = 4 K m_ubv_V = 19.40\n", + "M = 2600.000 Msun T = 2 K m_ubv_V = 26.99\n", + "M = 3400.000 Msun T = 0 K m_ubv_V = 24.07\n", + "M = 4100.000 Msun T = 4 K m_ubv_V = 20.39\n", + "M = 4900.000 Msun T = 2 K m_ubv_V = 18.84\n", + "M = 5000.000 Msun T = 2 K m_ubv_V = 18.80\n", + "M = 10500.000 Msun T = 3 K m_ubv_V = 15.55\n", + "M = 18000.000 Msun T = 5 K m_ubv_V = 14.54\n", + "M = 47000.000 Msun T = 4 K m_ubv_V = 12.82\n", + "M = 7750.000 Msun T = 1 K m_ubv_V = 16.40\n", + "M = 11000.000 Msun T = 2 K m_ubv_V = 15.49\n", + "M = 21000.000 Msun T = 3 K m_ubv_V = 14.24\n", + "M = 5500.000 Msun T = 2 K m_ubv_V = 18.12\n", + "M = 8000.000 Msun T = 2 K m_ubv_V = 16.31\n", + "M = 11250.000 Msun T = 4 K m_ubv_V = 15.43\n", + "M = 23000.000 Msun T = 4 K m_ubv_V = 14.13\n", + "M = 5750.000 Msun T = 3 K m_ubv_V = 17.92\n", + "M = 8250.000 Msun T = 4 K m_ubv_V = 16.25\n", + "M = 11750.000 Msun T = 4 K m_ubv_V = 15.37\n", + "M = 25000.000 Msun T = 5 K m_ubv_V = 14.00\n", + "M = 6000.000 Msun T = 4 K m_ubv_V = 17.74\n", + "M = 8750.000 Msun T = 2 K m_ubv_V = 16.04\n", + "M = 12250.000 Msun T = 4 K m_ubv_V = 15.29\n", + "M = 28000.000 Msun T = 4 K m_ubv_V = 13.78\n", + "M = 6250.000 Msun T = 4 K m_ubv_V = 17.59\n", + "M = 9000.000 Msun T = 4 K m_ubv_V = 15.97\n", + "M = 12750.000 Msun T = 4 K m_ubv_V = 15.11\n", + "M = 31000.000 Msun T = 4 K m_ubv_V = 13.45\n", + "M = 6750.000 Msun T = 0 K m_ubv_V = 16.97\n", + "M = 9500.000 Msun T = 3 K m_ubv_V = 15.76\n", + "M = 14000.000 Msun T = 3 K m_ubv_V = 14.91\n", + "M = 35000.000 Msun T = 4 K m_ubv_V = 13.18\n", + "M = 7000.000 Msun T = 2 K m_ubv_V = 16.81\n", + "M = 10000.000 Msun T = 2 K m_ubv_V = 15.61\n", + "M = 16000.000 Msun T = 3 K m_ubv_V = 14.62\n", + "M = 39000.000 Msun T = 4 K m_ubv_V = 13.00\n", + "M = 7250.000 Msun T = 2 K m_ubv_V = 16.65\n", + "M = 50000.000 Msun T = 8 K m_ubv_V = 12.73\n", + "M = 14000.000 Msun T = 8 K m_ubv_V = 14.94\n", + "M = 13750.000 Msun T = 8 K m_ubv_V = 14.97\n", + "M = 10750.000 Msun T = 7 K m_ubv_V = 15.46\n", + "M = 16500.000 Msun T = 8 K m_ubv_V = 14.65\n", + "M = 6750.000 Msun T = 9 K m_ubv_V = 17.09\n", + "M = 7000.000 Msun T = 7 K m_ubv_V = 16.93\n", + "M = 17250.000 Msun T = 10 K m_ubv_V = 14.58\n", + "M = 13250.000 Msun T = 8 K m_ubv_V = 15.04\n", + "M = 7750.000 Msun T = 8 K m_ubv_V = 16.49\n", + "M = 8500.000 Msun T = 7 K m_ubv_V = 16.11\n", + "M = 2601.716 Msun T = 7 K m_ubv_V = 23.84\n", + "M = 2601.716 Msun T = 8 K m_ubv_V = 23.84\n", + "M = 14736.126 Msun T = 10 K m_ubv_V = 14.69\n", + "M = 14736.126 Msun T = 11 K m_ubv_V = 14.69\n", + "Starting filter: ubv,R Elapsed time: 105.93 seconds\n", + "Starting synthetic photometry\n", + "M = 1000.000 Msun T = 2 K m_ubv_R = 36.49\n", + "M = 1000.000 Msun T = 4 K m_ubv_R = 36.49\n", + "M = 1000.000 Msun T = 4 K m_ubv_R = 36.49\n", + "M = 1000.000 Msun T = 6 K m_ubv_R = 36.49\n", + "M = 1400.000 Msun T = 4 K m_ubv_R = 28.95\n", + "M = 2600.000 Msun T = 3 K m_ubv_R = 24.72\n", + "M = 2600.000 Msun T = 4 K m_ubv_R = 21.53\n", + "M = 3400.000 Msun T = 2 K m_ubv_R = 21.02\n", + "M = 4100.000 Msun T = 6 K m_ubv_R = 19.22\n", + "M = 4900.000 Msun T = 4 K m_ubv_R = 18.47\n", + "M = 3000.000 Msun T = 2 K m_ubv_R = 21.60\n", + "M = 3800.000 Msun T = 0 K m_ubv_R = 20.51\n", + "M = 4500.000 Msun T = 4 K m_ubv_R = 18.90\n", + "M = 2600.000 Msun T = 2 K m_ubv_R = 22.86\n", + "M = 3400.000 Msun T = 0 K m_ubv_R = 21.09\n", + "M = 4100.000 Msun T = 5 K m_ubv_R = 19.35\n", + "M = 4900.000 Msun T = 3 K m_ubv_R = 18.39\n", + "M = 3000.000 Msun T = 1 K m_ubv_R = 22.15\n", + "M = 3700.000 Msun T = 6 K m_ubv_R = 19.98\n", + "M = 4500.000 Msun T = 4 K m_ubv_R = 18.83\n", + "M = 2600.000 Msun T = 2 K m_ubv_R = 23.90\n", + "M = 3300.000 Msun T = 6 K m_ubv_R = 20.87\n", + "M = 4100.000 Msun T = 4 K m_ubv_R = 19.38\n", + "M = 4900.000 Msun T = 2 K m_ubv_R = 18.40\n", + "M = 3000.000 Msun T = 0 K m_ubv_R = 24.15\n", + "M = 3700.000 Msun T = 4 K m_ubv_R = 20.06\n", + "M = 4500.000 Msun T = 2 K m_ubv_R = 18.84\n", + "M = 2900.000 Msun T = 2 K m_ubv_R = 23.91\n", + "M = 4100.000 Msun T = 6 K m_ubv_R = 19.37\n", + "M = 4900.000 Msun T = 4 K m_ubv_R = 18.30\n", + "M = 3000.000 Msun T = 2 K m_ubv_R = 24.20\n", + "M = 3700.000 Msun T = 6 K m_ubv_R = 20.30\n", + "M = 4500.000 Msun T = 4 K m_ubv_R = 18.77\n", + "M = 2600.000 Msun T = 2 K m_ubv_R = 25.38\n", + "M = 3400.000 Msun T = 0 K m_ubv_R = 22.82\n", + "M = 4100.000 Msun T = 4 K m_ubv_R = 19.64\n", + "M = 4900.000 Msun T = 2 K m_ubv_R = 18.26\n", + "M = 5000.000 Msun T = 2 K m_ubv_R = 18.38\n", + "M = 10500.000 Msun T = 3 K m_ubv_R = 15.56\n", + "M = 18000.000 Msun T = 5 K m_ubv_R = 14.61\n", + "M = 47000.000 Msun T = 4 K m_ubv_R = 12.95\n", + "M = 7750.000 Msun T = 1 K m_ubv_R = 16.35\n", + "M = 11000.000 Msun T = 2 K m_ubv_R = 15.49\n", + "M = 21000.000 Msun T = 3 K m_ubv_R = 14.32\n", + "M = 5500.000 Msun T = 2 K m_ubv_R = 17.76\n", + "M = 8000.000 Msun T = 2 K m_ubv_R = 16.26\n", + "M = 11250.000 Msun T = 4 K m_ubv_R = 15.46\n", + "M = 23000.000 Msun T = 4 K m_ubv_R = 14.22\n", + "M = 5750.000 Msun T = 3 K m_ubv_R = 17.61\n", + "M = 8250.000 Msun T = 4 K m_ubv_R = 16.19\n", + "M = 11750.000 Msun T = 4 K m_ubv_R = 15.39\n", + "M = 25000.000 Msun T = 5 K m_ubv_R = 14.10\n", + "M = 6000.000 Msun T = 4 K m_ubv_R = 17.46\n", + "M = 8750.000 Msun T = 2 K m_ubv_R = 16.02\n", + "M = 12250.000 Msun T = 4 K m_ubv_R = 15.32\n", + "M = 28000.000 Msun T = 4 K m_ubv_R = 13.88\n", + "M = 6250.000 Msun T = 4 K m_ubv_R = 17.32\n", + "M = 9000.000 Msun T = 4 K m_ubv_R = 15.95\n", + "M = 12750.000 Msun T = 4 K m_ubv_R = 15.14\n", + "M = 31000.000 Msun T = 4 K m_ubv_R = 13.57\n", + "M = 6750.000 Msun T = 0 K m_ubv_R = 16.81\n", + "M = 9500.000 Msun T = 3 K m_ubv_R = 15.76\n", + "M = 14000.000 Msun T = 3 K m_ubv_R = 14.95\n", + "M = 35000.000 Msun T = 4 K m_ubv_R = 13.30\n", + "M = 7000.000 Msun T = 2 K m_ubv_R = 16.66\n", + "M = 10000.000 Msun T = 2 K m_ubv_R = 15.60\n", + "M = 16000.000 Msun T = 3 K m_ubv_R = 14.68\n", + "M = 39000.000 Msun T = 4 K m_ubv_R = 13.13\n", + "M = 7250.000 Msun T = 2 K m_ubv_R = 16.51\n", + "M = 50000.000 Msun T = 8 K m_ubv_R = 12.86\n", + "M = 14000.000 Msun T = 8 K m_ubv_R = 14.99\n", + "M = 13750.000 Msun T = 8 K m_ubv_R = 15.02\n", + "M = 10750.000 Msun T = 7 K m_ubv_R = 15.48\n", + "M = 16500.000 Msun T = 8 K m_ubv_R = 14.71\n", + "M = 6750.000 Msun T = 9 K m_ubv_R = 16.86\n", + "M = 7000.000 Msun T = 7 K m_ubv_R = 16.72\n", + "M = 17250.000 Msun T = 10 K m_ubv_R = 14.64\n", + "M = 13250.000 Msun T = 8 K m_ubv_R = 15.09\n", + "M = 7750.000 Msun T = 8 K m_ubv_R = 16.35\n", + "M = 8500.000 Msun T = 7 K m_ubv_R = 16.03\n", + "M = 2601.716 Msun T = 7 K m_ubv_R = 22.65\n", + "M = 2601.716 Msun T = 8 K m_ubv_R = 22.65\n", + "M = 14736.126 Msun T = 10 K m_ubv_R = 14.67\n", + "M = 14736.126 Msun T = 11 K m_ubv_R = 14.67\n", + " Time taken: 154.51 seconds\n" + ] + } + ], + "source": [ + "# Fetch isochrone\n", + "logAge = 10 # Age in log(years)\n", + "AKs = 0 # Ks filter extinction in mags\n", + "dist = 4000 # distance in parsecs\n", + "metallicity = 0 # metallicity in [M/H]\n", + "atm_func = atmospheres.get_merged_atmosphere_w_bb_supplement\n", + "evo_merged = evolution.COSMIC(keep_COSMIC_tables=True)\n", + "redlaw = reddening.RedLawCardelli(3.1) # Rv = 3.1\n", + "filt_list = ['ubv,U', 'ubv,V', 'ubv,R']\n", + "\n", + "iso_dir = 'atm_cosmic/'\n", + "\n", + "if not os.path.exists(iso_dir):\n", + " os.mkdir(iso_dir)\n", + "\n", + "iso = synthetic.IsochronePhotExternalEvolution(logAge, AKs, dist, metallicity=metallicity,\n", + " evo_model=evo_merged, atm_func=atm_func,\n", + " filters=filt_list, red_law=redlaw,\n", + " atm_grid_dir=iso_dir, mass_sampling=3, recomp=False)" + ] + }, + { + "cell_type": "markdown", + "id": "40243f4b-5559-4a30-8a5b-cd000eea3fa0", + "metadata": {}, + "source": [ + "Next we can make the cluster. You must specify a resolved multiplicity object (i.e. with orbital parameters defined) and with a CSF_max = 1 and companion_max = True. This ensures there are only binaries and no higher order systems since COSMIC does not support them." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "4c12140e-88d8-48ba-98e2-0ba55e87d9e6", + "metadata": {}, + "outputs": [], + "source": [ + "# Now we can make the cluster. \n", + "clust_mtot = 10**4.\n", + "clust_multiplicity = multiplicity.MultiplicityResolvedDK(CSF_max = 1, companion_max = True)\n", + "\n", + "# Multiplicity is defined in the IMF object\n", + "clust_imf_Mult = imf.Kroupa_2001(multiplicity=clust_multiplicity)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "17d315a9-ec84-4289-a810-855443628e67", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/mambaforge3/envs/astro_cosmic/lib/python3.11/site-packages/cosmic/utils.py:1324: UserWarning: At least one of your initial binaries is starting in Roche Lobe Overflow:\n", + " kstar_1 kstar_2 mass_1 mass_2 porb ecc \\\n", + "982 1.0 1.0 34.983501 10.502985 0.737500 0.780722 \n", + "3145 1.0 1.0 26.931079 22.490881 0.386408 0.869985 \n", + "3221 1.0 1.0 112.879903 33.537283 0.674712 0.846808 \n", + "6147 1.0 0.0 2.719136 0.430439 0.258503 0.743511 \n", + "8163 1.0 1.0 51.934940 25.942593 0.104794 0.906464 \n", + "8976 1.0 1.0 1.353911 1.047359 0.270292 0.880324 \n", + "11341 1.0 0.0 2.210809 0.039488 0.243581 0.696509 \n", + "11997 1.0 1.0 56.384960 8.901185 0.376155 0.812874 \n", + "12177 1.0 1.0 867.905900 91.463500 0.054490 0.871674 \n", + "12473 1.0 1.0 89.947583 65.866705 0.194641 0.337963 \n", + "12907 1.0 1.0 5.848843 4.541931 0.195558 0.566851 \n", + "13526 1.0 1.0 282.145127 150.334490 0.037996 0.970647 \n", + "14116 1.0 1.0 23.279792 13.816783 0.373627 0.175221 \n", + "14810 1.0 1.0 4.124396 2.772040 0.423764 0.160526 \n", + "15923 1.0 1.0 94.804547 13.757087 0.039236 0.962300 \n", + "16181 1.0 1.0 33.320239 29.349840 0.081215 0.818545 \n", + "16402 1.0 1.0 1256.702679 283.551447 0.026743 0.330402 \n", + "\n", + " metallicity tphysf mass0_1 mass0_2 ... tacc_1 tacc_2 \\\n", + "982 0.02 10000.0 34.983501 10.502985 ... 0.0 0.0 \n", + "3145 0.02 10000.0 26.931079 22.490881 ... 0.0 0.0 \n", + "3221 0.02 10000.0 112.879903 33.537283 ... 0.0 0.0 \n", + "6147 0.02 10000.0 2.719136 0.430439 ... 0.0 0.0 \n", + "8163 0.02 10000.0 51.934940 25.942593 ... 0.0 0.0 \n", + "8976 0.02 10000.0 1.353911 1.047359 ... 0.0 0.0 \n", + "11341 0.02 10000.0 2.210809 0.039488 ... 0.0 0.0 \n", + "11997 0.02 10000.0 56.384960 8.901185 ... 0.0 0.0 \n", + "12177 0.02 10000.0 867.905900 91.463500 ... 0.0 0.0 \n", + "12473 0.02 10000.0 89.947583 65.866705 ... 0.0 0.0 \n", + "12907 0.02 10000.0 5.848843 4.541931 ... 0.0 0.0 \n", + "13526 0.02 10000.0 282.145127 150.334490 ... 0.0 0.0 \n", + "14116 0.02 10000.0 23.279792 13.816783 ... 0.0 0.0 \n", + "14810 0.02 10000.0 4.124396 2.772040 ... 0.0 0.0 \n", + "15923 0.02 10000.0 94.804547 13.757087 ... 0.0 0.0 \n", + "16181 0.02 10000.0 33.320239 29.349840 ... 0.0 0.0 \n", + "16402 0.02 10000.0 1256.702679 283.551447 ... 0.0 0.0 \n", + "\n", + " epoch_1 epoch_2 tms_1 tms_2 bhspin_1 bhspin_2 tphys binfrac \n", + "982 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "3145 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "3221 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "6147 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "8163 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "8976 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "11341 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "11997 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "12177 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "12473 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "12907 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "13526 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "14116 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "14810 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "15923 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "16181 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "16402 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 \n", + "\n", + "[17 rows x 38 columns]\n", + " warnings.warn(\n", + "/u/nsabrams/code/multiplicity/PyPopStar/spisea/evolution.py:1849: RuntimeWarning: divide by zero encountered in log10\n", + " return np.log10(((np.array(c.G.to('Rsun^3/(Msun*s^2)').value*masses/((radii)**2))*u.Rsun/u.s**2).to('cm/s^2')).value)\n", + "/u/nsabrams/code/multiplicity/PyPopStar/spisea/evolution.py:1849: RuntimeWarning: divide by zero encountered in log10\n", + " return np.log10(((np.array(c.G.to('Rsun^3/(Msun*s^2)').value*masses/((radii)**2))*u.Rsun/u.s**2).to('cm/s^2')).value)\n", + "/opt/mambaforge3/envs/astro_cosmic/lib/python3.11/site-packages/pandas/core/arraylike.py:402: RuntimeWarning: divide by zero encountered in log10\n", + " result = getattr(ufunc, method)(*inputs, **kwargs)\n", + "/opt/mambaforge3/envs/astro_cosmic/lib/python3.11/site-packages/pandas/core/arraylike.py:402: RuntimeWarning: invalid value encountered in log10\n", + " result = getattr(ufunc, method)(*inputs, **kwargs)\n" + ] + } + ], + "source": [ + "# Make cluster\n", + "clust_cosmic = synthetic.ResolvedCluster(iso, clust_imf_Mult, clust_mtot)" + ] + }, + { + "cell_type": "markdown", + "id": "43d8c754-54ff-49c8-a360-ffa5fbc86bd8", + "metadata": {}, + "source": [ + "If you specified `keep_COSMIC_tables=True`, then you can access the COSMIC tables as described here: https://cosmic-popsynth.github.io/COSMIC/pages/output_info.html#output-info" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "467f1672-1f16-4c43-8add-47d0953fcd05", + "metadata": {}, + "outputs": [], + "source": [ + "bcm_table = clust_cosmic.iso.evo_model.bcm\n", + "bpp_table = clust_cosmic.iso.evo_model.bpp" + ] + }, + { + "cell_type": "markdown", + "id": "10db81b9-2a29-41f3-af72-a02b941b6dad", + "metadata": {}, + "source": [ + "# Example analysis plots" + ] + }, + { + "cell_type": "markdown", + "id": "1e79d887-cb09-458c-a237-e1d76709f9d9", + "metadata": {}, + "source": [ + "CMD" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "45ac7b14-e09b-4840-b11d-647e2c5609e4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'm_ubv_R')" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(clust_cosmic.star_systems['m_ubv_V'] - clust_cosmic.star_systems['m_ubv_R'], clust_cosmic.star_systems['m_ubv_R'], marker = '.', linestyle = 'None')\n", + "plt.gca().invert_yaxis()\n", + "plt.xlabel('m_ubv_V - m_ubv_R')\n", + "plt.ylabel('m_ubv_R')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "77389803-b5ab-408d-a825-68c2ac30c131", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Singles Only')" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(clust_cosmic.star_systems['m_ubv_V'][clust_cosmic.star_systems['isMultiple'] == 0] - clust_cosmic.star_systems['m_ubv_R'][clust_cosmic.star_systems['isMultiple'] == 0], clust_cosmic.star_systems['m_ubv_R'][clust_cosmic.star_systems['isMultiple'] == 0], marker = '.', linestyle = 'None')\n", + "plt.gca().invert_yaxis()\n", + "plt.xlabel('m_ubv_V - m_ubv_R')\n", + "plt.ylabel('m_ubv_R')\n", + "plt.title('Singles Only')" + ] + }, + { + "cell_type": "markdown", + "id": "6f9f47e3-eb3c-4855-9d7e-a5e645c5229a", + "metadata": {}, + "source": [ + "Semimajor axis distribution. Note this goes down to lower semimajor axes than when we evolve each star individual since binary interaction is now taken into account." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "594beb18-cdee-4cc4-8d4b-64848ce4700e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 0, 'log(a) [AU]')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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d4OXcqLtsyxLubm5uuaaa5Sammp3OS3qo48+UpcuXeR0OjV58mStXbtWl156qd1ltZjVq1fr73//u/Ly8uwu5Zy1qoAye/ZsORyOMy7FxcXB/g899JA++OADbdq0SVFRUbr77rtlteIH54Z7/JJ0+PBh3Xjjjbrtttt077332lR582nKOWhPHA5HyLplWQ3a0PZNmzZNH374oV5++WW7S2lxF198sXbv3q2dO3fqgQce0IQJE/TJJ5/YXVaLOHjwoB588EGtWLFC559/vt3lnLNW9aj7I0eO6MiRI2fs06tXr0b/YA4dOiSv16sdO3a02uG+cI//8OHDysjI0MCBA7V06VKdd16ryqONasrfgaVLlyonJ0fHjx+PcHX2qaurU0xMjF599VWNGTMm2P7ggw9q9+7dKioqsrG6ludwOLR27VqNHj3a7lJaXHZ2ttatW6d33nlHKSkpdpdju6FDh6p379564YUX7C4l4tatW6cxY8YoKioq2FZfXy+Hw6HzzjtPgUAgZJvpbHtZYFMkJCQoISGhSZ/9NocFAoHmLKlFhXP8X331lTIyMpSWlqaCgoI2EU6kc/s70JZ17NhRaWlpKiwsDAkohYWFuuWWW2ysDC3FsixlZ2dr7dq12rZtG+Hk/7Esq1X/ux+OIUOG6KOPPgppu+eee9S3b1898sgjrSqcSK0soJyt999/X++//76uueYade3aVV9++aV+85vfqHfv3q129CQchw8f1uDBg9WzZ089/fTT+vrrr4PbPB6PjZW1rAMHDujYsWM6cOCA6uvrtXv3bknSj370I3Xp0sXe4iIgNzdXd911ly6//HINGjRIL774og4cONBu5h5VV1fr888/D67v379fu3fvVnx8vHr27GljZS1j6tSpWrVqldavX6/Y2Njg3COXy6VOnTrZXF3LePTRR5WVlSWv16uqqiqtXr1a27Zt09tvv213aS0iNja2wZyjb+dhtsq5SPbdQBQ5H374oZWRkWHFx8dbTqfT6tWrlzV58mTr0KFDdpfWIgoKCixJjS7tyYQJExo9B1u3brW7tIh59tlnreTkZKtjx47WT3/603Z1i+nWrVsb/fOeMGGC3aW1iNP9N19QUGB3aS3ml7/8ZfDvf/fu3a0hQ4ZYmzZtsrssW7Xm24xb1RwUAADQPrSNiQkAAKBNIaAAAADjEFAAAIBxCCgAAMA4BBQAAGAcAgoAADAOAQUAABiHgAIAAIxDQAHaucGDBysnJyci+77uuuu0atWqs+7/5ptvasCAATp16tQZ+02cODH49up169adY5WN27ZtW/A72uNLBwG7EVAARMSbb76p8vJy/fznPz/rz4wcOVIOh+OsQs2NN96osrIyZWVlNdj2q1/9SlFRUVq9enWDbRMnTmw0cOzevVsOh0OlpaWSpPT0dJWVlWncuHFnXT+A5kNAARARf/zjH3XPPfeE/Sbte+65RwsXLvzefk6nUx6PR06nM6T9m2++0Zo1a/TQQw9pyZIlYX33/+rYsaM8Hk+7edEeYBoCCoAQlZWVuvvuu9W1a1fFxMQoKytL+/btC+nzpz/9SV6vVzExMRozZozy8/P1gx/8ILj9yJEj2rx5s0aNGhXyufz8fPXr10+dO3eW1+vVlClTVF1dHdJn1KhRev/99/Xll182qf5XX31Vl156qWbNmqX33nsvOCICoHUhoAAIMXHiRBUXF+uNN97QX//6V1mWpZtuukknTpyQJL333nuaPHmyHnzwQe3evVvDhg3Tk08+GbKP7du3KyYmRpdccklI+3nnnac//vGP2rNnj5YtW6YtW7bo4YcfDumTnJysHj166N13321S/UuWLNGdd94pl8ulm266SQUFBU3aDwB7EVAABO3bt09vvPGG/vznP+vaa69V//79tXLlSn311VfByagLFy5UVlaWZsyYoYsuukhTpkxpMA+ktLRUbre7weWdnJwcZWRkKCUlRTfccIN+97vf6ZVXXmlQxwUXXNCkkY99+/Zp586dGj9+vCTpzjvvVEFBwfdOugVgHgIKgKBPP/1U0dHRGjhwYLCtW7duuvjii/Xpp59Kkvbu3asrr7wy5HPfXa+trdX555/fYP9bt27VsGHDdMEFFyg2NlZ33323jh49qpqampB+nTp10jfffBN2/UuWLNHw4cOVkJAgSbrppptUU1OjzZs3h70vAPYioAAIsizrtO0Oh6PBz6f7XEJCgiorK0Pa/vWvf+mmm25SamqqXnvtNZWUlOjZZ5+VpODlo28dO3ZM3bt3D6v2+vp6vfTSS9qwYYOio6MVHR2tmJgYHTt2LGSybFxcnHw+X4PPHz9+XJLkcrnC+l4AkRFtdwEAzHHppZfq5MmT+tvf/qb09HRJ0tGjR/XPf/4zOJ+kb9++ev/990M+V1xcHLI+YMAAlZeXq7KyUl27dg32OXnypJ555pngpZ/GLu/85z//0RdffKEBAwaEVfvGjRtVVVWlDz74QFFRUcH2zz77THfccYeOHj2qbt26qW/fvnr55Zf1n//8J2SUZ9euXerevXuwXgD2YgQFQFCfPn10yy236L777tP27dv1j3/8Q3feeacuuOAC3XLLLZKk7Oxsbdy4Ufn5+dq3b59eeOEFvfXWWyGjKgMGDFD37t313nvvBdt69+6tkydPauHChfryyy+1fPlyPf/88w1q2Llzp5xOpwYNGhRW7UuWLNGIESPUv39/paamBpdbb71V3bt314oVKyRJd9xxh6Kjo3XXXXepuLhYX3zxhVasWKG8vDw99NBDTTltACKAgAIgREFBgdLS0jRy5EgNGjRIlmVp48aN6tChgyTp6quv1vPPP6/8/Hz1799fb7/9tn7961+HjEZERUXpl7/8pVauXBls+8lPfqL8/Hw99dRTSk1N1cqVK5WXl9fg+19++WXdcccdiomJOeua//3vf2vDhg269dZbG2xzOBwaO3Zs8DKPy+XSu+++K8uyNHr0aPXv31/z5s3T7373O02fPv2svxNAZDms0110BoCzdN999+mzzz4LuTX43//+ty677DKVlJQoOTn5rPbz9ddfq2/fviouLlZKSspp+02cOFHHjx+P2GPu7fouAP8fIygAwvb000/rH//4hz7//HMtXLhQy5Yt04QJE0L6uN1uLVmyRAcOHDjr/e7fv1/PPffcGcPJt95880116dJFb775Ztj1n413331XXbp0CRkFAtByGEEBELZx48Zp27Ztqqqq0g9/+ENlZ2dr8uTJLfb9FRUV8vv9kqTExER17ty52b+jtrZWX331lSSpS5cu8ng8zf4dAE6PgAIAAIzDJR4AAGAcAgoAADAOAQUAABiHgAIAAIxDQAEAAMYhoAAAAOMQUAAAgHEIKAAAwDj/B90pkAGU9O69AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.hist(clust_cosmic.companions['log_a'], bins = 40)\n", + "plt.xlabel('log(a) [AU]')" + ] + }, + { + "cell_type": "markdown", + "id": "bf893097-e5f5-43c7-90a4-941deca662be", + "metadata": {}, + "source": [ + "Kick distribution. We add the kicks from COSMIC which come from supernovae prescriptions and account for if the objects are disrupted or not." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "cf7ede5c-417f-4342-8e6a-3b9efae1e43e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0, 0.5, 'N objects')" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjoAAAGwCAYAAACgi8/jAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAwv0lEQVR4nO3deVyV1b7H8e8WFSfEAUVRRMshCUccruZYimKpHRuszOmoHZNMonKIzPLaoc45lZVoV09Xm0746pRWZnVJTS3MAaU0srRDYQ5xNQXUAoV1/+jlvu0DGMOGvVn783699uvVs5611/PbS4Vvz7OeZzuMMUYAAAAWquHpAgAAACoLQQcAAFiLoAMAAKxF0AEAANYi6AAAAGsRdAAAgLUIOgAAwFo1PV2ApxUWFurYsWMKCAiQw+HwdDkAAKAUjDHKzc1VSEiIatQo+byNzwedY8eOKTQ01NNlAACAcjhy5Ihat25d4n6fDzoBAQGSfp2ohg0bergaAABQGjk5OQoNDXX+Hi+JzwedS5erGjZsSNABAKCa+b1lJyxGBgAA1iLoAAAAaxF0AACAtQg6AADAWgQdAABgLYIOAACwFkEHAABYy2eDTmJiosLDw9W7d29PlwIAACqJwxhjPF2EJ+Xk5CgwMFDZ2dk8MBAAgGqitL+/ffaMDgAAsB9BBwAAWIugAwAArEXQAQAA1iLoAAAAa9X0dAE2azv/vd/t890T11dBJQAA+CbO6AAAAGsRdAAAgLUIOgAAwFoEHQAAYC2CDgAAsBZBBwAAWIugAwAArEXQAQAA1iLoAAAAaxF0AACAtQg6AADAWtYEnfPnzyssLEwPPPCAp0sBAABewpqg8/jjj6tv376eLgMAAHgRK4LOoUOHdPDgQY0aNcrTpQAAAC/i8aCzbds2jR49WiEhIXI4HFq/fn2RPsuXL1e7du1Up04dRUZGavv27S77H3jgASUkJFRRxQAAoLrweNA5d+6cunXrpmXLlhW7f+3atYqNjVV8fLz27dungQMHKjo6WpmZmZKkt99+Wx07dlTHjh1Ldby8vDzl5OS4vAAAgJ1qerqA6OhoRUdHl7j/6aef1rRp0zR9+nRJ0tKlS/Xhhx9qxYoVSkhI0GeffaakpCS98cYbOnv2rC5cuKCGDRvqkUceKXa8hIQEPfbYY5XyWQAAgHfx+Bmdy8nPz1dqaqqioqJc2qOiopSSkiLp1+By5MgRfffdd/rb3/6mGTNmlBhyJGnBggXKzs52vo4cOVKpnwEAAHiOx8/oXM7JkydVUFCg4OBgl/bg4GCdOHGiXGP6+/vL39/fHeUBAAAv59VB5xKHw+GybYwp0iZJU6ZMqaKKAABAdeDVl66CgoLk5+dX5OxNVlZWkbM8ZZWYmKjw8HD17t27QuMAAADv5dVBp3bt2oqMjFRycrJLe3Jysvr371+hsWNiYpSenq7du3dXaBwAAOC9PH7p6uzZszp8+LBzOyMjQ2lpaWrSpInatGmjuLg4TZw4Ub169VK/fv20cuVKZWZmaubMmR6sGgAAVAceDzp79uzR0KFDndtxcXGSpMmTJ2vNmjUaP368Tp06pcWLF+v48eOKiIjQxo0bFRYW5qmSAQBANeHxoDNkyBAZYy7bZ9asWZo1a5Zbj5uYmKjExEQVFBS4dVwAAOA9vHqNTmVijQ4AAPbz2aADAADsR9ABAADW8tmgw3N0AACwn88GHdboAABgP58NOgAAwH4EHQAAYC2CDgAAsBZBBwAAWMtngw53XQEAYD+fDTrcdQUAgP18NugAAAD7EXQAAIC1CDoAAMBaBB0AAGAtnw063HUFAID9fDbocNcVAAD289mgAwAA7EfQAQAA1iLoAAAAaxF0AACAtQg6AADAWgQdAABgLZ8NOjxHBwAA+/ls0OE5OgAA2M9ngw4AALAfQQcAAFiLoAMAAKxF0AEAANYi6AAAAGsRdAAAgLUIOgAAwFoEHQAAYC2fDTo8GRkAAPv5bNDhycgAANjPZ4MOAACwH0EHAABYi6ADAACsRdABAADWIugAAABrEXQAAIC1CDoAAMBaBB0AAGAtgg4AALAWQQcAAFiLoAMAAKxF0AEAANby2aDDt5cDAGA/nw06fHs5AAD289mgAwAA7EfQAQAA1iLoAAAAaxF0AACAtQg6AADAWgQdAABgLYIOAACwFkEHAABYi6ADAACsRdABAADWIugAAABrEXQAAIC1CDoAAMBaBB0AAGAtgg4AALAWQQcAAFir2ged3Nxc9e7dW927d1eXLl20atUqT5cEAAC8RE1PF1BR9erV09atW1WvXj2dP39eERERGjdunJo2berp0gAAgIdV+zM6fn5+qlevniTpl19+UUFBgYwxHq4KAAB4A48HnW3btmn06NEKCQmRw+HQ+vXri/RZvny52rVrpzp16igyMlLbt2932X/mzBl169ZNrVu31ty5cxUUFFRF1QMAAG/m8aBz7tw5devWTcuWLSt2/9q1axUbG6v4+Hjt27dPAwcOVHR0tDIzM519GjVqpM8//1wZGRn6xz/+oR9//LGqygcAAF7M40EnOjpaS5Ys0bhx44rd//TTT2vatGmaPn26OnfurKVLlyo0NFQrVqwo0jc4OFhdu3bVtm3bSjxeXl6ecnJyXF4AAMBOHg86l5Ofn6/U1FRFRUW5tEdFRSklJUWS9OOPPzrDSk5OjrZt26ZOnTqVOGZCQoICAwOdr9DQ0Mr7AAAAwKO8OuicPHlSBQUFCg4OdmkPDg7WiRMnJEk//PCDBg0apG7dumnAgAG655571LVr1xLHXLBggbKzs52vI0eOVOpnAAAAnlMtbi93OBwu28YYZ1tkZKTS0tJKPZa/v7/8/f3dWR4AAPBSXn1GJygoSH5+fs6zN5dkZWUVOctTVomJiQoPD1fv3r0rNA4AAPBeXh10ateurcjISCUnJ7u0Jycnq3///hUaOyYmRunp6dq9e3eFxgEAAN7L45euzp49q8OHDzu3MzIylJaWpiZNmqhNmzaKi4vTxIkT1atXL/Xr108rV65UZmamZs6c6cGqAQBAdeDxoLNnzx4NHTrUuR0XFydJmjx5stasWaPx48fr1KlTWrx4sY4fP66IiAht3LhRYWFhnioZAABUEx4POkOGDPndr2yYNWuWZs2a5dbjJiYmKjExUQUFBW4dFwAAeA+vXqNTmVijAwCA/Xw26AAAAPsRdAAAgLUIOgAAwFo+G3R4YCAAAPbz2aDDYmQAAOzns0EHAADYj6ADAACsRdABAADW8tmgw2JkAADs57NBh8XIAADYz2eDDgAAsB9BBwAAWIugAwAArEXQAQAA1iLoAAAAa/ls0OH2cgAA7OezQYfbywEAsJ/PBh0AAGA/gg4AALAWQQcAAFiLoAMAAKxF0AEAANby2aDD7eUAANjPZ4MOt5cDAGA/nw06AADAfgQdAABgLYIOAACwFkEHAABYi6ADAACsRdABAADWIugAAABrVTjoFBQUKC0tTadPn3ZHPQAAAG5T5qATGxurF198UdKvIWfw4MHq2bOnQkND9fHHH7u7vkrDk5EBALBfmYPOP//5T3Xr1k2S9O677yojI0MHDx5UbGys4uPj3V5gZeHJyAAA2K/MQefkyZNq0aKFJGnjxo265ZZb1LFjR02bNk379+93e4EAAADlVeagExwcrPT0dBUUFOiDDz7QsGHDJEnnz5+Xn5+f2wsEAAAor5plfcPUqVN16623qmXLlnI4HBo+fLgkaefOnbrqqqvcXiAAAEB5lTnoPProo4qIiNCRI0d0yy23yN/fX5Lk5+en+fPnu71AAACA8ipz0Hn55Zc1fvx4Z8C55Pbbb1dSUpLbCgMAAKioMq/RmTp1qrKzs4u05+bmaurUqW4pCgAAwB3KHHSMMXI4HEXaf/jhBwUGBrqlKAAAAHco9aWrHj16yOFwyOFw6LrrrlPNmv//1oKCAmVkZGjkyJGVUiQAAEB5lDro3HjjjZKktLQ0jRgxQg0aNHDuq127ttq2baubbrrJ7QUCAACUV6mDzqJFiyRJbdu21W233VZkMTIAAIC3KfManfDwcKWlpRVp37lzp/bs2eOOmgAAANyizEEnJiZGR44cKdJ+9OhRxcTEuKUoAAAAdyhz0ElPT1fPnj2LtPfo0UPp6eluKaoq8O3lAADYr8xBx9/fXz/++GOR9uPHj7vcieXt+PZyAADsV+agM3z4cC1YsMDloYFnzpzRQw895PzeKwAAAG9Q5lMwTz31lAYNGqSwsDD16NFD0q+3nAcHB+uVV15xe4EAAADlVeag06pVK33xxRd67bXX9Pnnn6tu3bqaOnWqbr/9dtWqVasyagQAACiXci2qqV+/vu666y531wIAAOBWZV6jI0mvvPKKBgwYoJCQEH3//feSpGeeeUZvv/22W4sDAACoiDIHnRUrViguLk7R0dE6ffq0CgoKJEmNGzfW0qVL3V0fAABAuZU56Dz//PNatWqV4uPjXW4n79Wrl/bv3+/W4gAAACqizEEnIyPDebfVb/n7++vcuXNuKQoAAMAdyhx02rVrV+x3Xb3//vsKDw93R00AAABuUea7rh588EHFxMTol19+kTFGu3bt0uuvv66EhAT9/e9/r4waAQAAyqXMQWfq1Km6ePGi5s6dq/Pnz+uOO+5Qq1at9Oyzz+q2226rjBoBAADKpVzP0ZkxY4ZmzJihkydPqrCwUM2bN3d3XQAAABVWoW/hDAoKclcdAAAAbleqoNOzZ09t2rRJjRs3Vo8ePeRwOErs26BBA1199dV66KGHFBoa6rZCAQAAyqpUQWfs2LHy9/eXJN14442X7ZuXl6dNmzbpzjvv1NatWytcIAAAQHmVKugsWrSo2P8uybfffqurr766/FUBAAC4QbnX6GRlZenrr7+Ww+FQx44dXRYkX3nllfrxxx/dUiAAAEB5lfmBgTk5OZo4caJatWqlwYMHa9CgQWrVqpXuvPNOZWdnO/sFBga6tdCSHDlyREOGDFF4eLi6du2qN954o0qOCwAAvF+Zg8706dO1c+dObdiwQWfOnFF2drY2bNigPXv2aMaMGZVR42XVrFlTS5cuVXp6uj766CPdd999fBUFAACQVI5LV++9954+/PBDDRgwwNk2YsQIrVq1SiNHjnRrcaXRsmVLtWzZUpLUvHlzNWnSRD/99JPq169f5bUAAADvUuYzOk2bNi32slRgYKAaN25c5gK2bdum0aNHKyQkRA6HQ+vXry/SZ/ny5WrXrp3q1KmjyMhIbd++vdix9uzZo8LCQm5rBwAAksoRdB5++GHFxcXp+PHjzrYTJ07owQcf1MKFC8tcwLlz59StWzctW7as2P1r165VbGys4uPjtW/fPg0cOFDR0dHKzMx06Xfq1ClNmjRJK1euvOzx8vLylJOT4/ICAAB2chhjzO91+veHBB46dEh5eXlq06aNJCkzM1P+/v7q0KGD9u7dW/5iHA6tW7fO5Vk9ffv2Vc+ePbVixQpnW+fOnXXjjTcqISFB0q/hZfjw4ZoxY4YmTpx42WM8+uijeuyxx4q0Z2dnq2HDhuWuvTht57/3u32+e+J6tx4TAABfkJOTo8DAwN/9/V2qNTq/95DAypKfn6/U1FTNnz/fpT0qKkopKSmSJGOMpkyZomuvvfZ3Q44kLViwQHFxcc7tnJwcLnUBAGCpMj8wsCqdPHlSBQUFCg4OdmkPDg7WiRMnJEmffvqp1q5dq65duzrX97zyyivq0qVLsWP6+/s7n/IMAADsVu4HBqampuqrr76Sw+FQeHi4evTo4c66XPz7d2sZY5xtAwYMUGFhYaUdGwAAVF9lDjpZWVm67bbb9PHHH6tRo0Yyxig7O1tDhw5VUlKSmjVr5rbigoKC5Ofn5zx789sa/v0sT1klJiYqMTFRBQUFFRoHAAB4rzLfdTV79mzl5OToyy+/1E8//aTTp0/rwIEDysnJ0b333uvW4mrXrq3IyEglJye7tCcnJ6t///4VGjsmJkbp6enavXt3hcYBAADeq8xndD744AN99NFH6ty5s7MtPDxciYmJioqKKnMBZ8+e1eHDh53bGRkZSktLU5MmTdSmTRvFxcVp4sSJ6tWrl/r166eVK1cqMzNTM2fOLPOxAACAbylz0CksLFStWrWKtNeqVatca2X27NmjoUOHOrcv3RE1efJkrVmzRuPHj9epU6e0ePFiHT9+XBEREdq4caPCwsLKfCwAAOBbSvUcnd8aO3aszpw5o9dff10hISGSpKNHj2rChAlq3Lix1q1bVymFuttv1+h88803PEcHAIBqpLTP0SnzGp1ly5YpNzdXbdu21ZVXXqn27durXbt2ys3N1fPPP1+hoqsSa3QAALBfmS9dhYaGau/evUpOTtbBgwdljFF4eLiGDRtWGfUBAACUW7mfozN8+HANHz7cnbUAAAC4VZkvXQEAAFQXPht0EhMTFR4ert69e3u6FAAAUEl8NuiwGBkAAPv5bNABAAD2I+gAAABrlfquqxo1ahT5FvF/53A4dPHixQoXBQAA4A6lDjqXe+JxSkqKnn/+eZXxIcsexbeXAwBgvzJ/BcRvHTx4UAsWLNC7776rCRMm6D//8z/Vpk0bd9ZX6Ur7COny4CsgAACoHJX2FRCSdOzYMc2YMUNdu3bVxYsXlZaWppdeeqnahRwAAGC3MgWd7OxszZs3T+3bt9eXX36pTZs26d1331VERERl1QcAAFBupV6j85e//EVPPvmkWrRooddff11jx46tzLoAAAAqrNRrdGrUqKG6detq2LBh8vPzK7HfW2+95bbiqgJrdAAAqH5K+/u71Gd0Jk2a9Lu3l1cn3HUFAID9KnTXlQ04owMAQPVTqXddAQAAVAcEHQAAYC2CDgAAsBZBBwAAWIugAwAArEXQAQAA1vLZoJOYmKjw8HD17t3b06UAAIBK4rNBJyYmRunp6dq9e7enSwEAAJXEZ4MOAACwH0EHAABYi6ADAACsRdABAADWIugAAABrEXQAAIC1CDoAAMBaBB0AAGAtnw06PBkZAAD7+WzQ4cnIAADYz2eDDgAAsB9BBwAAWIugAwAArEXQAQAA1iLoAAAAaxF0AACAtQg6AADAWgQdAABgLYIOAACwFkEHAABYi6ADAACsRdABAADW8tmgw7eXAwBgP58NOnx7OQAA9vPZoAMAAOxH0AEAANYi6AAAAGsRdAAAgLUIOgAAwFoEHQAAYC2CDgAAsBZBBwAAWIugAwAArEXQAQAA1iLoAAAAaxF0AACAtQg6AADAWgQdAABgLYIOAACwFkEHAABYi6ADAACsZUXQ+cMf/qDGjRvr5ptv9nQpAADAi1gRdO699169/PLLni4DAAB4GSuCztChQxUQEODpMgAAgJfxeNDZtm2bRo8erZCQEDkcDq1fv75In+XLl6tdu3aqU6eOIiMjtX379qovFAAAVDseDzrnzp1Tt27dtGzZsmL3r127VrGxsYqPj9e+ffs0cOBARUdHKzMzs1zHy8vLU05OjssLAADYyeNBJzo6WkuWLNG4ceOK3f/0009r2rRpmj59ujp37qylS5cqNDRUK1asKNfxEhISFBgY6HyFhoZWpHwAAODFPB50Lic/P1+pqamKiopyaY+KilJKSkq5xlywYIGys7OdryNHjrijVAAA4IVqerqAyzl58qQKCgoUHBzs0h4cHKwTJ044t0eMGKG9e/fq3Llzat26tdatW6fevXsXO6a/v7/8/f0rtW4AAOAdvDroXOJwOFy2jTEubR9++GFVlwQAAKoBr750FRQUJD8/P5ezN5KUlZVV5CxPWSUmJio8PLzEMz8AAKD68+qgU7t2bUVGRio5OdmlPTk5Wf3796/Q2DExMUpPT9fu3bsrNA4AAPBeHr90dfbsWR0+fNi5nZGRobS0NDVp0kRt2rRRXFycJk6cqF69eqlfv35auXKlMjMzNXPmTA9WDQAAqgOPB509e/Zo6NChzu24uDhJ0uTJk7VmzRqNHz9ep06d0uLFi3X8+HFFRERo48aNCgsL81TJAACgmvB40BkyZIiMMZftM2vWLM2aNcutx01MTFRiYqIKCgrcOi4AAPAeXr1GpzKxRgcAAPv5bNABAAD2I+gAAABrEXQAAIC1fDbo8MBAAADs57NBh8XIAADYz2eDDgAAsB9BBwAAWIugAwAArOWzQYfFyAAA2M9ngw6LkQEAsJ/PBh0AAGA/gg4AALAWQQcAAFiLoAMAAKzls0GHu64AALCfzwYd7roCAMB+Pht0AACA/Qg6AADAWgQdAABgLYIOAACwFkEHAABYi6ADAACs5bNBh+foAABgP58NOjxHBwAA+/ls0AEAAPYj6AAAAGsRdAAAgLUIOgAAwFoEHQAAYC2CDgAAsBZBBwAAWIugAwAArOWzQYcnIwMAYD+fDTo8GRkAAPv5bNABAAD2I+gAAABrEXQAAIC1CDoAAMBaBB0AAGAtgg4AALAWQQcAAFiLoAMAAKxF0AEAANYi6AAAAGsRdAAAgLUIOgAAwFo+G3T49nIAAOzns0GHby8HAMB+Pht0AACA/Qg6AADAWgQdAABgLYIOAACwFkEHAABYi6ADAACsRdABAADWIugAAABrEXQAAIC1CDoAAMBaBB0AAGAtgg4AALAWQQcAAFiLoAMAAKxF0AEAANYi6AAAAGsRdAAAgLWsCDobNmxQp06d1KFDB/3973/3dDkAAMBL1PR0ARV18eJFxcXFacuWLWrYsKF69uypcePGqUmTJp4uDQAAeFi1P6Oza9cuXX311WrVqpUCAgI0atQoffjhh54uCwAAeAGPB51t27Zp9OjRCgkJkcPh0Pr164v0Wb58udq1a6c6deooMjJS27dvd+47duyYWrVq5dxu3bq1jh49WhWlAwAAL+fxoHPu3Dl169ZNy5YtK3b/2rVrFRsbq/j4eO3bt08DBw5UdHS0MjMzJUnGmCLvcTgcJR4vLy9POTk5Li8AAGAnj6/RiY6OVnR0dIn7n376aU2bNk3Tp0+XJC1dulQffvihVqxYoYSEBLVq1crlDM4PP/ygvn37ljheQkKCHnvsMfd9gApqO/89t4zz3RPXu+VY3jaOu5SmHm9T2vmpyj+z0qjKY7lLdawZ8AbV4d+Ox8/oXE5+fr5SU1MVFRXl0h4VFaWUlBRJUp8+fXTgwAEdPXpUubm52rhxo0aMGFHimAsWLFB2drbzdeTIkUr9DAAAwHM8fkbnck6ePKmCggIFBwe7tAcHB+vEiROSpJo1a+qpp57S0KFDVVhYqLlz56pp06Yljunv7y9/f/9KrRsAAHgHrw46l/z7mhtjjEvbmDFjNGbMmKouCwAAeDmvvnQVFBQkPz8/59mbS7Kysoqc5SmrxMREhYeHq3fv3hUaBwAAeC+vDjq1a9dWZGSkkpOTXdqTk5PVv3//Co0dExOj9PR07d69u0LjAAAA7+XxS1dnz57V4cOHndsZGRlKS0tTkyZN1KZNG8XFxWnixInq1auX+vXrp5UrVyozM1MzZ870YNUAAKA68HjQ2bNnj4YOHercjouLkyRNnjxZa9as0fjx43Xq1CktXrxYx48fV0REhDZu3KiwsDBPlQwAAKoJjwedIUOGFPvQv9+aNWuWZs2a5dbjJiYmKjExUQUFBW4dFwAAeA+vXqNTmVijAwCA/Xw26AAAAPsRdAAAgLV8NujwHB0AAOzns0GHNToAANjPZ4MOAACwH0EHAABYy+PP0fG0S8/wycnJcfvYhXnn3T5mSUpTf2nq8bZx3KUy/nwrW2nnpyr/zEqjKo/lLtWxZsAbePLfzqVxf+9ZfA7zez0s98MPPyg0NNTTZQAAgHI4cuSIWrduXeJ+nw86hYWFOnbsmAICAuRwONw2bk5OjkJDQ3XkyBE1bNjQbePahnkqPeaqdJin0mGeSod5Kh1PzJMxRrm5uQoJCVGNGiWvxPH5S1c1atS4bBKsqIYNG/KPoxSYp9JjrkqHeSod5ql0mKfSqep5CgwM/N0+LEYGAADWIugAAABrEXQqib+/vxYtWiR/f39Pl+LVmKfSY65Kh3kqHeapdJin0vHmefL5xcgAAMBenNEBAADWIugAAABrEXQAAIC1CDoAAMBaBJ1Ksnz5crVr10516tRRZGSktm/f7umSqkxCQoJ69+6tgIAANW/eXDfeeKO+/vprlz7GGD366KMKCQlR3bp1NWTIEH355ZcuffLy8jR79mwFBQWpfv36GjNmjH744Yeq/ChVKiEhQQ6HQ7Gxsc425ulXR48e1Z133qmmTZuqXr166t69u1JTU537madfXbx4UQ8//LDatWununXr6oorrtDixYtVWFjo7OOLc7Vt2zaNHj1aISEhcjgcWr9+vct+d83J6dOnNXHiRAUGBiowMFATJ07UmTNnKvnTuc/l5unChQuaN2+eunTpovr16yskJESTJk3SsWPHXMbwynkycLukpCRTq1Yts2rVKpOenm7mzJlj6tevb77//ntPl1YlRowYYVavXm0OHDhg0tLSzPXXX2/atGljzp496+zzxBNPmICAAPPmm2+a/fv3m/Hjx5uWLVuanJwcZ5+ZM2eaVq1ameTkZLN3714zdOhQ061bN3Px4kVPfKxKtWvXLtO2bVvTtWtXM2fOHGc782TMTz/9ZMLCwsyUKVPMzp07TUZGhvnoo4/M4cOHnX2Yp18tWbLENG3a1GzYsMFkZGSYN954wzRo0MAsXbrU2ccX52rjxo0mPj7evPnmm0aSWbdunct+d83JyJEjTUREhElJSTEpKSkmIiLC3HDDDVX1MSvscvN05swZM2zYMLN27Vpz8OBBs2PHDtO3b18TGRnpMoY3zhNBpxL06dPHzJw506XtqquuMvPnz/dQRZ6VlZVlJJmtW7caY4wpLCw0LVq0ME888YSzzy+//GICAwPNCy+8YIz59R9VrVq1TFJSkrPP0aNHTY0aNcwHH3xQtR+gkuXm5poOHTqY5ORkM3jwYGfQYZ5+NW/ePDNgwIAS9zNP/+/66683f/zjH13axo0bZ+68805jDHNljCnyC9xdc5Kenm4kmc8++8zZZ8eOHUaSOXjwYCV/KvcrLhD+u127dhlJzv+J99Z54tKVm+Xn5ys1NVVRUVEu7VFRUUpJSfFQVZ6VnZ0tSWrSpIkkKSMjQydOnHCZI39/fw0ePNg5R6mpqbpw4YJLn5CQEEVERFg3jzExMbr++us1bNgwl3bm6VfvvPOOevXqpVtuuUXNmzdXjx49tGrVKud+5un/DRgwQJs2bdI333wjSfr888/1ySefaNSoUZKYq+K4a0527NihwMBA9e3b19nnP/7jPxQYGGjlvEm//mx3OBxq1KiRJO+dJ5//Uk93O3nypAoKChQcHOzSHhwcrBMnTnioKs8xxiguLk4DBgxQRESEJDnnobg5+v777519ateurcaNGxfpY9M8JiUlae/evdq9e3eRfczTr/71r39pxYoViouL00MPPaRdu3bp3nvvlb+/vyZNmsQ8/ca8efOUnZ2tq666Sn5+fiooKNDjjz+u22+/XRJ/p4rjrjk5ceKEmjdvXmT85s2bWzlvv/zyi+bPn6877rjD+SWe3jpPBJ1K4nA4XLaNMUXafME999yjL774Qp988kmRfeWZI5vm8ciRI5ozZ47+53/+R3Xq1Cmxn6/PU2FhoXr16qU///nPkqQePXroyy+/1IoVKzRp0iRnP1+fJ0lau3atXn31Vf3jH//Q1VdfrbS0NMXGxiokJESTJ0929mOuinLHnBTX38Z5u3Dhgm677TYVFhZq+fLlv9vf0/PEpSs3CwoKkp+fX5FkmpWVVeT/GGw3e/ZsvfPOO9qyZYtat27tbG/RooUkXXaOWrRoofz8fJ0+fbrEPtVdamqqsrKyFBkZqZo1a6pmzZraunWrnnvuOdWsWdP5OX19nlq2bKnw8HCXts6dOyszM1MSf59+68EHH9T8+fN12223qUuXLpo4caLuu+8+JSQkSGKuiuOuOWnRooV+/PHHIuP/7//+r1XzduHCBd16663KyMhQcnKy82yO5L3zRNBxs9q1aysyMlLJycku7cnJyerfv7+Hqqpaxhjdc889euutt7R582a1a9fOZX+7du3UokULlznKz8/X1q1bnXMUGRmpWrVqufQ5fvy4Dhw4YM08Xnfdddq/f7/S0tKcr169emnChAlKS0vTFVdcwTxJuuaaa4o8nuCbb75RWFiYJP4+/db58+dVo4brj3U/Pz/n7eXMVVHumpN+/fopOztbu3btcvbZuXOnsrOzrZm3SyHn0KFD+uijj9S0aVOX/V47T5WyxNnHXbq9/MUXXzTp6ekmNjbW1K9f33z33XeeLq1K3H333SYwMNB8/PHH5vjx487X+fPnnX2eeOIJExgYaN566y2zf/9+c/vttxd7O2fr1q3NRx99ZPbu3Wuuvfbaan2La2n89q4rY5gnY369s6NmzZrm8ccfN4cOHTKvvfaaqVevnnn11VedfZinX02ePNm0atXKeXv5W2+9ZYKCgszcuXOdfXxxrnJzc82+ffvMvn37jCTz9NNPm3379jnvFnLXnIwcOdJ07drV7Nixw+zYscN06dKlWt1efrl5unDhghkzZoxp3bq1SUtLc/nZnpeX5xzDG+eJoFNJEhMTTVhYmKldu7bp2bOn89ZqXyCp2Nfq1audfQoLC82iRYtMixYtjL+/vxk0aJDZv3+/yzg///yzueeee0yTJk1M3bp1zQ033GAyMzOr+NNUrX8POszTr959910TERFh/P39zVVXXWVWrlzpsp95+lVOTo6ZM2eOadOmjalTp4654oorTHx8vMsvIl+cqy1bthT7M2ny5MnGGPfNyalTp8yECRNMQECACQgIMBMmTDCnT5+uok9ZcZebp4yMjBJ/tm/ZssU5hjfOk8MYYyrnXBEAAIBnsUYHAABYi6ADAACsRdABAADWIugAAABrEXQAAIC1CDoAAMBaBB0AAGAtgg4AALAWQQeoRoYMGaLY2NgS90+ZMkU33nhjqcb67rvv5HA4lJaW5pbavEHbtm21dOnSShn79+a+tDZv3qyrrrrK+f1Tjz76qLp3717hcSsiKytLzZo109GjRz1aB1AZCDqARZ599lmtWbPG02V4zO7du3XXXXc5tx0Oh9avX++5gooxd+5cxcfHF/nyzcoyZMgQvfDCC5ft07x5c02cOFGLFi2qkpqAqkTQASwSGBioRo0aeboMj2nWrJnq1avn6TJKlJKSokOHDumWW26pkuP99NNPSklJ0ejRo3+379SpU/Xaa6/p9OnTVVAZUHUIOkA19sEHHygwMFAvv/yypKKXrgoLC/Xkk0+qffv28vf3V5s2bfT4448XO1ZhYaFmzJihjh076vvvvy+2z6Xx//znPys4OFiNGjXSY489posXL+rBBx9UkyZN1Lp1a/33f/+3y/vmzZunjh07ql69erriiiu0cOFCXbhwwaXPkiVL1Lx5cwUEBGj69OmaP3++yyWdS8f+29/+ppYtW6pp06aKiYlxGee3l67atm0rSfrDH/4gh8Ph3C7u8l5sbKyGDBni3D537pwmTZqkBg0aqGXLlnrqqaeKzEV+fr7mzp2rVq1aqX79+urbt68+/vjjYuftkqSkJEVFRalOnTol9snIyFD79u119913q7CwUGvWrFGjRo20YcMGderUSfXq1dPNN9+sc+fO6aWXXlLbtm3VuHFjzZ49WwUFBS5jvffee+rWrZtatWql06dPa8KECWrWrJnq1q2rDh06aPXq1c6+Xbp0UYsWLbRu3brLfgaguqnp6QIAlE9SUpLuuusuvfLKKxo7dmyxfRYsWKBVq1bpmWee0YABA3T8+HEdPHiwSL/8/Hzdcccd+vbbb/XJJ5+oefPmJR538+bNat26tbZt26ZPP/1U06ZN044dOzRo0CDt3LlTa9eu1cyZMzV8+HCFhoZKkgICArRmzRqFhIRo//79mjFjhgICAjR37lxJ0muvvabHH39cy5cv1zXXXKOkpCQ99dRTateuncuxt2zZopYtW2rLli06fPiwxo8fr+7du2vGjBlF6ty9e7eaN2+u1atXa+TIkfLz8yv13D744IPasmWL1q1bpxYtWuihhx5SamqqS/CaOnWqvvvuOyUlJSkkJETr1q3TyJEjtX//fnXo0KHYcbdt26bbb7+9xOMeOHBAUVFRmjx5shISEpzt58+f13PPPaekpCTl5uZq3LhxGjdunBo1aqSNGzfqX//6l2666SYNGDBA48ePd77vnXfecf7dWLhwodLT0/X+++8rKChIhw8f1s8//+xy/D59+mj79u364x//WOq5ArxepX0vOgC3Gzx4sJkzZ45JTEw0gYGBZvPmzS77J0+ebMaOHWuMMSYnJ8f4+/ubVatWFTtWRkaGkWS2b99uhg0bZq655hpz5syZyx5/8uTJJiwszBQUFDjbOnXqZAYOHOjcvnjxoqlfv755/fXXSxznL3/5i4mMjHRu9+3b18TExLj0ueaaa0y3bt2KHPvixYvOtltuucWMHz/euR0WFmaeeeYZ57Yks27duiKf4dIcXTJnzhwzePBgY4wxubm5pnbt2iYpKcm5/9SpU6Zu3bpmzpw5xhhjDh8+bBwOhzl69KjLONddd51ZsGBBiZ87MDDQvPzyyy5tixYtMt26dTMpKSmmSZMm5q9//avL/tWrVxtJ5vDhw862P/3pT6ZevXomNzfX2TZixAjzpz/9ybn9yy+/mICAAPPFF18YY4wZPXq0mTp1aom1GWPMfffdZ4YMGXLZPkB1wxkdoJp588039eOPP+qTTz5Rnz59Suz31VdfKS8vT9ddd91lx7v99tvVunVrbdq0qVTrW66++mqXhbTBwcGKiIhwbvv5+alp06bKyspytv3zn//U0qVLdfjwYZ09e1YXL15Uw4YNnfu//vprzZo1y+U4ffr00ebNm4sc+7dnZlq2bKn9+/f/bs1l8e233yo/P1/9+vVztjVp0kSdOnVybu/du1fGGHXs2NHlvXl5eWratGmJY//888/FXrbKzMzUsGHDtGTJEt13331F9terV09XXnmlczs4OFht27ZVgwYNXNp+O+ebN29W06ZN1aVLF0nS3XffrZtuukl79+5VVFSUbrzxRvXv39/lOHXr1tX58+dLrB+ojlijA1Qz3bt3V7NmzbR69WoZY0rsV7du3VKNN2rUKH3xxRf67LPPStW/Vq1aLtsOh6PYtku3T3/22We67bbbFB0drQ0bNmjfvn2Kj49Xfn5+kff8VnGf7XLHKa0aNWoUGfu363wuN6eXFBYWys/PT6mpqUpLS3O+vvrqKz377LMlvi8oKKjYxb7NmjVTnz59lJSUpJycnCL7yzrnkutlK0mKjo7W999/r9jYWB07dkzXXXedHnjgAZcxfvrpJzVr1uzyHx6oZgg6QDVz5ZVXasuWLXr77bc1e/bsEvt16NBBdevW1aZNmy473t13360nnnhCY8aM0datW91drj799FOFhYUpPj5evXr1UocOHYosdu7UqZN27drl0rZnz54KH7tWrVpFFug2a9ZMx48fd2n77bOE2rdvr1q1arkEv9OnT+ubb75xbvfo0UMFBQXKyspS+/btXV4tWrQosZ4ePXooPT29SHvdunW1YcMG1alTRyNGjFBubm5ZP6oLY4zeffddjRkzxqW9WbNmmjJlil599VUtXbpUK1eudNl/4MAB9ejRo0LHBrwNQQeohjp27KgtW7bozTffLPEhdnXq1NG8efM0d+5cvfzyy/r222/12Wef6cUXXyzSd/bs2VqyZIluuOEGffLJJ26ttX379srMzFRSUpK+/fZbPffcc0Xu7Jk9e7ZefPFFvfTSSzp06JCWLFmiL774oshZnrJq27atNm3apBMnTjjPpFx77bXas2ePXn75ZR06dEiLFi3SgQMHnO9p0KCBpk2bpgcffFCbNm3SgQMHNGXKFJfLdR07dtSECRM0adIkvfXWW8rIyNDu3bv15JNPauPGjSXWM2LEiBLnt379+nrvvfdUs2ZNRUdH6+zZs+X+3KmpqTp37pwGDRrkbHvkkUf09ttv6/Dhw/ryyy+1YcMGde7c2bn//PnzSk1NVVRUVLmPC3gjgg5QTXXq1EmbN2/W66+/rvvvv7/YPgsXLtT999+vRx55RJ07d9b48eNd1nH8VmxsrB577DGNGjVKKSkpbqtz7Nixuu+++3TPPfeoe/fuSklJ0cKFC136TJgwQQsWLNADDzygnj17KiMjQ1OmTLnsbdil8dRTTyk5OVmhoaHOMxUjRozQwoULNXfuXPXu3Vu5ubmaNGmSy/v++te/atCgQRozZoyGDRumAQMGKDIy0qXP6tWrNWnSJN1///3q1KmTxowZo507dzrvNCvOnXfeqfT0dH399dfF7m/QoIHef/99GWM0atQonTt3rlyf++2339b111+vmjX/fxlm7dq1tWDBAnXt2lWDBg2Sn5+fkpKSXN7Tpk0bDRw4sFzHBLyVw5TmgjQAVLHhw4erRYsWeuWVVzxdilvNnTtX2dnZ+q//+q9KO0bXrl318MMP69Zbby31e/r06aPY2FjdcccdlVYX4AncdQXA486fP68XXnhBI0aMkJ+fn15//XV99NFHSk5O9nRpbhcfH6/ExEQVFBSU6dk+pZWfn6+bbrpJ0dHRpX5PVlaWbr755ss+4weorjijA8Djfv75Z40ePVp79+5VXl6eOnXqpIcffljjxo3zdGkAqjmCDgAAsBaLkQEAgLUIOgAAwFoEHQAAYC2CDgAAsBZBBwAAWIugAwAArEXQAQAA1iLoAAAAa/0fPsWBMbU6E34AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.hist(clust_cosmic.star_systems['kick'], bins = 50)\n", + "plt.xlabel('kick magnitude (km/s)')\n", + "plt.yscale('log')\n", + "plt.ylabel('N objects')" + ] + }, + { + "cell_type": "markdown", + "id": "516ccebd-2ad7-4dfe-8b24-919707a168e0", + "metadata": {}, + "source": [ + "# Check interpolation over atmosphere grid" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "ce24f553-6020-4000-be02-13feb6c7ba39", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.interpolate import LinearNDInterpolator" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "a9b3f917-bf60-4c99-bb57-4270541733f9", + "metadata": {}, + "outputs": [], + "source": [ + "interp = LinearNDInterpolator((clust_cosmic.iso.points['Teff'], clust_cosmic.iso.points['logg']), clust_cosmic.iso.points['m_ubv_R'],\n", + " fill_value=np.nan)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "dade2fb6-ae07-4597-9db7-5bfee462599d", + "metadata": {}, + "outputs": [], + "source": [ + "Teff_grid = 10**np.linspace(np.log10(clust_cosmic.iso.points['Teff'].min()), np.log10(clust_cosmic.iso.points['Teff'].max()), 50)\n", + "logg_grid = np.linspace(clust_cosmic.iso.points['logg'].min(), clust_cosmic.iso.points['logg'].max(), 50)\n", + "Z_grid = np.zeros(50)\n", + "\n", + "TT, GG = np.meshgrid(Teff_grid, logg_grid, indexing='ij')\n", + "\n", + "# Evaluate interpolator\n", + "values = interp(TT, GG)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "13d68eb1-b495-4051-a150-4c63ddeefc2a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAioAAAHFCAYAAADcytJ5AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABFUklEQVR4nO3de3wU9b3/8fckQEggiUIhIRAlSgABEY4gAip4SRTEG6fUihfU1kKDCKJiEZCIkgD15ETFQrEcTA+N+GsVr4DEHkhVRAMFoVQRa4qRksYLTQJCQnbn9wdldQ2ZvUySmWRfz8djHjXf73xnvrvdkM9+vpcxTNM0BQAA4EJRTncAAACgIQQqAADAtQhUAACAaxGoAAAA1yJQAQAArkWgAgAAXItABQAAuBaBCgAAcC0CFQAA4FoEKoADcnJy9NJLL9m6hmEYys7O9v28efNmGYahzZs3+8qys7NlGEbI17799tvVs2dPv7LG6HNzWLNmjQYNGqT27dsrJSVFM2bM0OHDh53uFoAwEagADmiuP/o//elP9e6774bcbt68eVq7dq1fWUsIVH73u9/ppptu0tChQ7V+/XrNnz9fzz77rMaPH+901wCEqY3THQDQdHr06KEePXqE3O7ss89ugt40LY/HowceeECZmZl65plnJEmXXnqp4uPjdfPNN2v9+vUaM2aMw70EECoyKsC/nRwm2bVrlyZMmKDExER16tRJM2fOVF1dnfbu3aurrrpK8fHx6tmzp5YsWVLvGlVVVbr//vuVlpamdu3aqXv37poxY4aOHDniO8cwDB05ckQFBQUyDEOGYWj06NGSpC+++EJZWVnq16+fOnbsqK5du+qyyy7TW2+9Zes1fV9hYaGGDx+ujh07qmPHjho0aJBWrlzpq//+0E9Dff773/+uNm3aKDc3t949/vSnP8kwDP3+978Pq++h2rp1qw4ePKg77rjDr3zChAnq2LFjvQwRgJaBjArwPT/60Y90yy23aPLkySoqKtKSJUt0/Phxvfnmm8rKytL999+vwsJCPfjgg+rVq5dvWOGbb77RqFGj9Pnnn+uhhx7SwIEDtWfPHj388MPavXu33nzzTRmGoXfffVeXXXaZLr30Us2bN0+SlJCQIEn6+uuvJUnz589XcnKyDh8+rLVr12r06NH64x//6Ato7Hj44Yf16KOPavz48brvvvuUmJiov/zlL9q/f3+DbRrqc8+ePXXttddq+fLlmjVrlqKjo31tli5dqpSUFN1www2W/amrqwuq39HR0Zbzbf7yl79IkgYOHOhX3rZtW/Xt29dXD6CFMQGYpmma8+fPNyWZ//Vf/+VXPmjQIFOS+eKLL/rKjh8/bnbp0sUcP368ryw3N9eMiooyS0pK/Nr/4Q9/MCWZ69at85V16NDBnDRpUsA+1dXVmcePHzcvv/xy84YbbvCrk2TOnz/f9/OmTZtMSeamTZvqvaaTPv30UzM6Otq8+eabLe87adIk88wzz/Qra6jPJ++7du1aX9mBAwfMNm3amI888kjA1ygpqGPVqlWW11m4cKEpyTx48GC9uszMTLN3794B+wLAfcioAN8zbtw4v5/POeccffDBB37zG9q0aaNevXr5ZSFee+01DRgwQIMGDfLLElx55ZW+1TjBzJFYvny5VqxYob/+9a+qqanxlfft29fOy5IkFRUVyePxaOrUqbavddLo0aN13nnn6emnn9b1118v6cRrMAxDP/vZzwK2LykpCeo+aWlpQZ3XUNYlnNVPAJxHoAJ8T6dOnfx+bteuneLi4tS+fft65VVVVb6f//nPf+qTTz5R27ZtT3ndL7/8MuC98/LydN9992nKlCl69NFH9YMf/EDR0dGaN2+ePvzwwzBejb8vvvhCksKaYGvlnnvu0U9/+lPt3btXZ511lp555hn98Ic/VHJycsC2gwYNCuoe3x1WOpXOnTtLkr766islJSX51X399df1/n8F0DIQqACN5Ac/+IFiY2P1P//zPw3WB7J69WqNHj1ay5Yt8yuvrq5ulD526dJFkvT5558rNTW1Ua4pSRMnTtSDDz6op59+WhdeeKHKy8uDzto0FNh936pVq3T77bc3WH/uuedKknbv3q1+/fr5yuvq6vTRRx/ppptuCuo+ANyFQAVoJOPGjVNOTo46d+4ccJgiJiZGR48erVduGIZiYmL8ynbt2qV33323UQKLzMxMRUdHa9myZRo+fHhIbRvqsyS1b99eP/vZz7R06VJt2bJFgwYN0siRI4O6bmMN/QwbNkzdunXTs88+qxtvvNFX/oc//EGHDx9mLxWghSJQARrJjBkz9MILL+iSSy7Rvffeq4EDB8rr9eqzzz7Txo0bdd9992nYsGGSTnz737x5s1599VV169ZN8fHx6tOnj8aN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SWkpPLI3fGNzJ6AWg0AFAAAHMPQTHAIVAAAcYPfBgpHyUMLIeJUAAKBFIqMCAIADTBny2pijYrI8GQAANBWGfoITGa8SAAC0SGRUABe7cvB8y3qjzmKZ6PEAy489LDEFnOQ1DXnN8IdvQm3bs2dP7d+/v155VlaWnn76aZmmqUceeUQrVqzQoUOHNGzYMD399NPq379/2H1sDGRUAABwgOffT0+2c4SipKREBw8e9B1FRUWSpAkTJkiSlixZory8PC1dulQlJSVKTk5WRkaGqqurG/21h4JABQCACNClSxclJyf7jtdee01nn322Ro0aJdM0lZ+frzlz5mj8+PEaMGCACgoK9M0336iwsNDRfhOoAADggJNDP3YOSaqqqvI7ampqAt67trZWq1ev1p133inDMFRaWqry8nJlZmb6zomJidGoUaO0ZcuWJnsPgkGgAgCAA7yKsn1IUmpqqhITE31Hbm5uwHu/9NJL+te//qXbb79dklReXi5JSkpK8jsvKSnJV+cUJtMCANCClZWVKSEhwfdzTExMwDYrV67UmDFjlJKS4lduGP4TdE3TrFfW3AhUAABwgMc05LGx6udk24SEBL9AJZD9+/frzTff1IsvvugrS05OlnQis9KtWzdfeUVFRb0sS3MjUAGaUOaFCyzrjePWzz81aq2XGBu1xxuu5Am3lowo65Fv08v7h6bV3MuTT1q1apW6du2qq6++2leWlpam5ORkFRUVafDgwZJOzGMpLi7W4sWLw+5jYyBQAQDAAabNpyebYbT1er1atWqVJk2apDZtvg0BDMPQjBkzlJOTo/T0dKWnpysnJ0dxcXGaOHFi2H1sDAQqAABEiDfffFOfffaZ7rzzznp1s2bN0tGjR5WVleXb8G3jxo2Kj493oKffIlABAMABHhny2HiwYDhtMzMzZZrmKesMw1B2drays7PD7lNTIFABAMABXjP8eSYn20cC9lEBAACuRUYFAAAHeG1OprXTtiUhUAEAwAFeGfLamKNip21LQqAC2BBwn5SaAPuk1AXYJ6XOur0amBQHNCkjwDd59vBBIyJQAQDAAY21M21rR6ACAIADmKMSnMh4lQAAoEUiowIAgAO8svmsHybTAgCApmLaXPVjEqgAAICm4tTTk1saAhXAQsaIxyzrozzWy4ONQMuHWcUJAJYIVAAAcACrfoJDoAIAgAMY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//nPVVdX1yjLiMeNG6dx48bZvg4AAJHs/vvv1zXXXKMzzjhDFRUVeuyxx1RVVaVJkybJMAzNmDFDOTk5Sk9PV3p6unJychQXF6eJEyeGdJ+f/vSn2rlzp1577TUNHz5chmFoy5Ytmj59uu666y79v//3/0K6XsiByttvv63HHnus3j4qv/71r7Vx40a98MILGjhwoJ588smI2e/EW97bsv73hxMt64v+1d+y/i9fXWZZX/G19fp2778a3g2wTXV0g3WS1OaYZbWi6qzrjQD18aWHLetrO1kvnzbsjO82AjNATtL24uKoAFcIbTsCtCSB1q6b3ubpRwtU5P29010ITjMvT/78889100036csvv1SXLl104YUXauvWrb55n7NmzdLRo0eVlZWlQ4cOadiwYdq4cWPIe6i8/vrreuONN3TRRRf5yq688ko988wzuuqqq0K6lhRGoPLGG29o8eLF9covv/xy3XfffZKksWPH6he/+EXInQEAIGI08xb6a9assaw3DEPZ2dnKzs4Ov0+SOnfurMTE+l/QExMTdfrpp4d8vZDnqHTq1EmvvvpqvfJXX31VnTp1kiQdOXIk5AgMAAC0fHPnztXMmTN18OBBX1l5ebkeeOABzZs3L+TrhZxRmTdvnn7+859r06ZNuuCCC2QYht5//32tW7dOy5cvlyQVFRVp1KhRIXcGAICI0YoeSjh48GC/Zcz79u3TmWeeqTPOOEOS9NlnnykmJkZffPFFyLvchhyo3HXXXerXr5+WLl2qF198UaZpqm/fvn4zg08OAQEAgFP77u6y4bZ3i+uvv77Jrh3Whm8jR47UyJEjG7svAACgBZo/f36TXTusQMXj8eill17Shx9+KMMw1K9fP1177bV+T2EEAAAWWtHQT1MKOVD55JNPNHbsWB04cEB9+vSRaZr6+OOPlZqaqtdff11nn312U/SzSX31jx6W9VuOdbKs33p4gGX9X6pSLOvLqk6zrP9XZZxlvbe6rWV99NGG50xHN/zsKkmSEeDpx4GWH7c7bL2E0gywNfPxjtbBb/SxAEs0A6zeC3R/w7T5L4Gr934G4KhWGqhERUVZbrvv8YS2r0LIgco999yjs88+W1u3bvWt8vnqq690yy236J577tHrr78e6iUBAEArsXbtWr+fjx8/rh07dqigoECPPPJIyNcLOVApLi72C1KkE2um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vbs6dOigYcOG+TIsmzdvVnx8vCTpsssu82VfTlUGAHYQqAAtxO23365t27bplVde0bvvvivTNDV27FgdP35ckvTOO+9oypQpmj59unbu3KmMjAwtXLgw7Pvdcccdeuedd7RmzRrt2rVLEyZM0FVXXaV9+/ZpxIgR2rt3ryTphRde0MGDBxssAwA7GPoBWoB9+/bplVde0TvvvOP74/+73/1OqampeumllzRhwgQ99dRTGjNmjO6//35JUu/evbVlyxa99tprId/vb3/7m5577jl9/vnnviGc+++/Xxs2bNCqVauUk5Ojrl27SjoxPJScnCxJpywDADsIVIAW4MMPP1SbNm00bNgwX1nnzp3Vp08fffjhh5KkvXv36oYbbvBrd8EFF4QVqPz5z3+WaZrq3bu3X3lNTY06d+4cxisAgPAQqAAtgGmaDZYbhlHvvwO1C8Tr9So6Olrbt29XdHS0X13Hjh3DuiYAhINABWgB+vXrp7q6Or333nu+oZ+vvvpKH3/8sc455xxJUt++ffX+++/7tdu2bVtY9xs8eLA8Ho8qKip08cUX2+s8ANjAZFqgBUhPT9d1112nu+66S2+//bY++OAD3XLLLerevbuuu+46SdK0adO0bt065eXlad++ffr1r3+t9evX18uyBKN37966+eabddttt+nFF19UaWmpSkpKtHjxYq1bt66xXx4ANIhABWghVq1apfPPP1/jxo3T8OHDZZqm1q1bp7Zt20qSRo4cqeXLlysvL0/nnXeeNmzYoHvvvVft27cP+3633Xab7rvvPvXp00fXXnut3nvvPaWmpjbmywIAS4YZ7iA2ANe766679NFHH+mtt95yuisAEBbmqACtyOOPP66MjAx16NBB69evV0FBgX71q1853S0ACBsZFaAV+dGPfqTNmzerurpaZ511lqZNm6YpU6Y43S0ACBuBCgAAcC0m0wIAANciUAEAAK5FoAIAAFyLQAUAALgWgQoAAHAtAhUAAOBaBCoAAMC1CFQAAIBrEagAAADX+v+MOkDCrADCdgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Choose nearest metallicity slice\n", + "plt.figure()\n", + "plt.pcolormesh(\n", + " np.log10(Teff_grid),\n", + " logg_grid,\n", + " values[:, :].T,\n", + " shading='auto'\n", + ")\n", + "plt.xlabel('log Teff')\n", + "plt.ylabel('log g')\n", + "plt.colorbar(label='m_ubv_R')\n", + "plt.title(f'metallicity = 0')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74efc7b1-7cae-4781-9c05-9dccb8adbde4", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [conda env:astro_cosmic]", + "language": "python", + "name": "conda-env-astro_cosmic-py" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/atmo_models.rst b/docs/atmo_models.rst index fda94778..b54056bc 100644 --- a/docs/atmo_models.rst +++ b/docs/atmo_models.rst @@ -4,7 +4,7 @@ Atmosphere Model Object ======================================== Stellar atmosphere models are defined as functions in -``popstar/atmospheres.py``. These can be called by:: +``spisea/atmospheres.py``. These can be called by:: from popstar import atmospheres atmo = atmospheres. diff --git a/docs/contributors.rst b/docs/contributors.rst index 93404981..bfd4c49e 100644 --- a/docs/contributors.rst +++ b/docs/contributors.rst @@ -19,7 +19,7 @@ Dongwon Kim -- code testing/debugging Siyao Jia -- helped with early code and documentation development Natasha Abrams -- developed resolved multiplicity capabilities -(ResolvedMultiplicityDK class) +(ResolvedMultiplicityDK class) and added COSMIC support. Michael Medford -- developed resolved multiplicity capabilities (ResolvedMultiplicityDK class) @@ -50,4 +50,4 @@ and increased filter name flexibility for synphot Anna Pusack -- Added IRTF L-band filter support -Caitlin Begbie -- added brown dwarf physics and capabilities \ No newline at end of file +Caitlin Begbie -- added brown dwarf physics and capabilities diff --git a/docs/contributors_BACKUP_57328.rst b/docs/contributors_BACKUP_57328.rst deleted file mode 100644 index 741f832f..00000000 --- a/docs/contributors_BACKUP_57328.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. _contributors: - -============ -Contributors -============ -Jessica Lu -- Lead Developer; developed initial package framework; collaboratively developed all aspects of code - -Matthew Hosek Jr -- Lead Developer; collaboratively developed all aspects of code - -Casey Lam -- implemented first IFMR module (IFMR_Raithel18) - -Abhimat Gautam -- implemented non-solar metallicity evolution model -support for MIST models as well as blackbody atmosphere function - -Kelly Lockhart -- helped develop UnresolvedCluster class - -Dongwon Kim -- code testing/debugging - -Siyao Jia -- helped with early code and documentation development - -Natasha Abrams -- developed resolved multiplicity capabilities -(ResolvedMultiplicityDK class) - -Michael Medford -- developed resolved multiplicity capabilities -(ResolvedMultiplicityDK class) - -Sam Rose -- developed metallicity-dependent IFMRs (IFMR_Spera15 and IFMR_N20_Sukhbold) - -Rebecca Lewis -- added HAWK-I filter support, code testing/debugging - -Manuel Parra-Royón -- installation using Docker containers and Singularity - -Javier Moldón -- installation using Docker containers and Singularity - -Winston Zhang -- bugfix to make redlaw paths correct regardless of -what operating system is used - -<<<<<<< HEAD -Caitlin Begbie -- added brown dwarf physics and capabilities -======= -Sage Hironaka Remulla -- added Rubin Observatory filters - -Lingfeng Wei -- bugfix to improve creation of iso_dir in -IsochronePhot, implemented faster cluster generation and test -functions for primary and companion star mass generation (v2.3), -updated random state generators (v2.4) - -Macy Huston -- New metallicity bound + isochrone filter checks, -imf_mass_lim bugfix, roman filter bugfix, added Euclid filters, Synthpop compatibility -updates (v2.2), improvements for reading in/updated existing isochrone -files, Vega mag to ST mag conversion function (v2.4) - -Anna Pusack -- Added IRTF L-band filter support ->>>>>>> upstream/dev diff --git a/docs/contributors_BASE_57328.rst b/docs/contributors_BASE_57328.rst deleted file mode 100644 index 40edaee7..00000000 --- a/docs/contributors_BASE_57328.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. _contributors: - -============ -Contributors -============ -Jessica Lu -- Lead Developer; developed initial package framework; collaboratively developed all aspects of code - -Matthew Hosek Jr -- Lead Developer; collaboratively developed all aspects of code - -Casey Lam -- implemented first IFMR module (IFMR_Raithel18) - -Abhimat Gautam -- implemented non-solar metallicity evolution model -support for MIST models as well as blackbody atmosphere function - -Kelly Lockhart -- helped develop UnresolvedCluster class - -Dongwon Kim -- code testing/debugging - -Siyao Jia -- helped with early code and documentation development - -Natasha Abrams -- developed resolved multiplicity capabilities -(ResolvedMultiplicityDK class) - -Michael Medford -- developed resolved multiplicity capabilities -(ResolvedMultiplicityDK class) - -Sam Rose -- developed metallicity-dependent IFMRs (IFMR_Spera15 and IFMR_N20_Sukhbold) - -Rebecca Lewis -- added HAWK-I filter support, code testing/debugging - -Manuel Parra-Royón -- installation using Docker containers and Singularity - -Javier Moldón -- installation using Docker containers and Singularity - -Winston Zhang -- bugfix to make redlaw paths correct regardless of -what operating system is used diff --git a/docs/contributors_LOCAL_57328.rst b/docs/contributors_LOCAL_57328.rst deleted file mode 100644 index e62072d7..00000000 --- a/docs/contributors_LOCAL_57328.rst +++ /dev/null @@ -1,38 +0,0 @@ -.. _contributors: - -============ -Contributors -============ -Jessica Lu -- Lead Developer; developed initial package framework; collaboratively developed all aspects of code - -Matthew Hosek Jr -- Lead Developer; collaboratively developed all aspects of code - -Casey Lam -- implemented first IFMR module (IFMR_Raithel18) - -Abhimat Gautam -- implemented non-solar metallicity evolution model -support for MIST models as well as blackbody atmosphere function - -Kelly Lockhart -- helped develop UnresolvedCluster class - -Dongwon Kim -- code testing/debugging - -Siyao Jia -- helped with early code and documentation development - -Natasha Abrams -- developed resolved multiplicity capabilities -(ResolvedMultiplicityDK class) - -Michael Medford -- developed resolved multiplicity capabilities -(ResolvedMultiplicityDK class) - -Sam Rose -- developed metallicity-dependent IFMRs (IFMR_Spera15 and IFMR_N20_Sukhbold) - -Rebecca Lewis -- added HAWK-I filter support, code testing/debugging - -Manuel Parra-Royón -- installation using Docker containers and Singularity - -Javier Moldón -- installation using Docker containers and Singularity - -Winston Zhang -- bugfix to make redlaw paths correct regardless of -what operating system is used - -Caitlin Begbie -- added brown dwarf physics and capabilities diff --git a/docs/contributors_REMOTE_57328.rst b/docs/contributors_REMOTE_57328.rst deleted file mode 100644 index 1944147e..00000000 --- a/docs/contributors_REMOTE_57328.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. _contributors: - -============ -Contributors -============ -Jessica Lu -- Lead Developer; developed initial package framework; collaboratively developed all aspects of code - -Matthew Hosek Jr -- Lead Developer; collaboratively developed all aspects of code - -Casey Lam -- implemented first IFMR module (IFMR_Raithel18) - -Abhimat Gautam -- implemented non-solar metallicity evolution model -support for MIST models as well as blackbody atmosphere function - -Kelly Lockhart -- helped develop UnresolvedCluster class - -Dongwon Kim -- code testing/debugging - -Siyao Jia -- helped with early code and documentation development - -Natasha Abrams -- developed resolved multiplicity capabilities -(ResolvedMultiplicityDK class) - -Michael Medford -- developed resolved multiplicity capabilities -(ResolvedMultiplicityDK class) - -Sam Rose -- developed metallicity-dependent IFMRs (IFMR_Spera15 and IFMR_N20_Sukhbold) - -Rebecca Lewis -- added HAWK-I filter support, code testing/debugging - -Manuel Parra-Royón -- installation using Docker containers and Singularity - -Javier Moldón -- installation using Docker containers and Singularity - -Winston Zhang -- bugfix to make redlaw paths correct regardless of -what operating system is used - -Sage Hironaka Remulla -- added Rubin Observatory filters - -Lingfeng Wei -- bugfix to improve creation of iso_dir in -IsochronePhot, implemented faster cluster generation and test -functions for primary and companion star mass generation (v2.3), -updated random state generators (v2.4) - -Macy Huston -- New metallicity bound + isochrone filter checks, -imf_mass_lim bugfix, roman filter bugfix, added Euclid filters, Synthpop compatibility -updates (v2.2), improvements for reading in/updated existing isochrone -files, Vega mag to ST mag conversion function (v2.4) - -Anna Pusack -- Added IRTF L-band filter support diff --git a/docs/evo_models.rst b/docs/evo_models.rst index 4cabc35c..d066ae2e 100644 --- a/docs/evo_models.rst +++ b/docs/evo_models.rst @@ -73,11 +73,18 @@ Below is a table of the evolution model grids currently supported by SPISEA. - 6.00 - 10.00 - -0.5, 0, 0.5 - Marley et al. (2021) + * - ``COSMIC`` :sup:`b` + - 0.08 - 150 + - - + - -2.3 - 0.18 + - Breivik et al. (2020) .. rubric:: Footnotes :sup:`a` Actual maximum value given by the Sonora models (Marley et al., 2021) relies on the age of the cluster. For example, for log(Age)=6.0, the mass range is limited to 0.0005 - 0.011 M\ :sub:`⊙`. +:sup:`b` COSMIC evolves the stars externally and does not use SPISEA's standard isochrone-grid architecture. Instead, it uses a custom atmosphere grid that is created on the fly. See Breivik et al. (2020) for more details on COSMIC. When COSMIC is used, the IFMR is ignored. + Please note the stellar mass range, age range, and metallicity values of the evolution model grid you choose: @@ -142,4 +149,7 @@ Specific Evolution Model Classes :show-inheritance: .. autoclass:: evolution.Phillips2020 + :show-inheritance: + +.. autoclass:: evolution.COSMIC :show-inheritance: \ No newline at end of file diff --git a/docs/make_isochrone.rst b/docs/make_isochrone.rst index 5f764c93..11ae9bd1 100644 --- a/docs/make_isochrone.rst +++ b/docs/make_isochrone.rst @@ -89,11 +89,18 @@ Tips and Tricks: The IsochronePhot Object reddening law have changed, the original file will be overwritten by the new isochrone. - *To avoid files from being unintentially overwritten, we recommend + *To avoid files from being unintentionally overwritten, we recommend that users specify different iso_dir paths when making isochrones with different evolution models, atmosphere models, or reddening laws.* +* For external evolution models (i.e. COSMIC), you should use + IsochronePhotExternalEvolution + instead of IsochronePhot. This is because those evolution models do not have isochrones but + instead evolve the stars externally. The first time you run a new AKs, metallicity, or distance, + this will take ~10-20 mins because it is creating a new atmosphere grid. This table is saved in the + specified iso_dir, under the filename atm___.fits. + Base Isochrone Class ---------------------------- .. autoclass:: synthetic.Isochrone @@ -108,6 +115,10 @@ Isochrone Sub-classes :show-inheritance: :members: make_photometry, plot_CMD, plot_mass_magnitude +.. autoclass:: synthetic.IsochronePhotExternalEvolution + :show-inheritance: + :members: make_photometry, plot_CMD, plot_mass_magnitude + Photometry Conversion Functions ----------------------------- diff --git a/docs/multiplicity.rst b/docs/multiplicity.rst index 69011c8c..1b7709f9 100644 --- a/docs/multiplicity.rst +++ b/docs/multiplicity.rst @@ -27,6 +27,11 @@ returned in the ``star_systems`` table off the cluster object is the same for both unresolved and resolved multiplicity classes: it represents the combined photometry of all stars within a given system. +For most selected evolution models, the multiples are evolved as single stars. +To evolve binaries (does not support higher order multiples), you should use one of the ``MultiplicityResolved`` classes +and the ``COSMIC`` evolution model. +See the example jupyter notebook `Cluster_w_COSMIC.ipynb `_ for an example. + Unresolved Multiplicity Classes ------------------------------------------ diff --git a/spisea/atmospheres.py b/spisea/atmospheres.py index 489c0078..f5612d35 100755 --- a/spisea/atmospheres.py +++ b/spisea/atmospheres.py @@ -16,21 +16,7 @@ def get_atmosphere_bounds(model_dir, metallicity=0, temperature=20000, gravity=4 """ Given atmosphere model, get temperature and gravity bounds """ - # Open catalog fits file and break out row indices - catalog = Table.read('{0}/grid/{1}/catalog.fits'.format(os.environ['PYSYN_CDBS'], model_dir)) - - teff_arr = [] - z_arr = [] - logg_arr = [] - for cur_row_index in range(len(catalog)): - index = catalog['INDEX'][cur_row_index] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) + teff_arr, z_arr, logg_arr = get_atmosphere_grid(model_dir) # Filter by metallicity. Will chose the closest metallicity to desired input metal_list = np.unique(np.array(z_arr)) @@ -94,6 +80,45 @@ def get_atmosphere_bounds(model_dir, metallicity=0, temperature=20000, gravity=4 return (temperature_new, gravity_new, metallicity_new) +def get_atmosphere_grid(model_dir): + """ + Gets grid of temps, Zs, and loggs of atmosphere grid + + Parameters + ---------- + model_dir : str + Model directory + + Returns + ------- + teff_arr : array-like + Temperature array + + z_arr : array-like + Metallicity array + + logg_arr : array-like + Surface gravity array + """ + # Open catalog fits file and break out row indices + catalog = Table.read('{0}/grid/{1}/catalog.fits'.format(os.environ['PYSYN_CDBS'], model_dir)) + + teff_arr = [] + z_arr = [] + logg_arr = [] + for cur_row_index in range(len(catalog)): + index = catalog['INDEX'][cur_row_index] + tmp = index.split(',') + teff_arr.append(float(tmp[0])) + z_arr.append(float(tmp[1])) + logg_arr.append(float(tmp[2])) + teff_arr = np.array(teff_arr) + z_arr = np.array(z_arr) + logg_arr = np.array(logg_arr) + + return teff_arr, z_arr, logg_arr + + def get_kurucz_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): """ Return atmosphere from the Kurucz pysnphot grid @@ -119,6 +144,7 @@ def get_kurucz_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=Fal rebin: boolean Always false for this particular function """ + try: sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) except: @@ -140,6 +166,20 @@ def get_kurucz_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=Fal return sp +def get_kurucz_atmosphere_grid(): + """ + Return atmosphere grid from the Kurucz pysnphot grid + (`Kurucz 1993 `_). + + Grid Range: + + * Teff: 3000 - 50000 K + * gravity: 0 - 5 cgs + * metallicity: -5.0 - 1.0 + """ + teff_arr, z_arr, logg_arr = get_atmosphere_grid('k93models') + return teff_arr, z_arr, logg_arr + def get_castelli_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): """ Return atmospheres from the pysynphot ATLAS9 atlas @@ -191,12 +231,30 @@ def get_castelli_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=F return sp +def get_castelli_atmosphere_grid(): + """ + Return atmosphere grid from the pysynphot ATLAS9 atlas + (`Castelli & Kurucz 2004 `_). + + Grid Range: + + * Teff: 3500 - 50000 K + * gravity: 0 - 5.0 cgs + * [M/H]: -2.5 - 0.2 + """ + + teff_arr, z_arr, logg_arr = get_atmosphere_grid('ck04models') + return teff_arr, z_arr, logg_arr + def get_nextgen_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): """ metallicity = [M/H] (def = 0) temperature = Kelvin (def = 5000) gravity = log gravity (def = 4.0) """ + if get_grid_only: + teff_arr, z_arr, logg_arr = get_atmosphere_grid('nextgen') + return teff_arr, z_arr, logg_arr try: sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) except: @@ -218,12 +276,22 @@ def get_nextgen_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=Fal return sp +def get_nextgen_atmosphere_grid(): + """ + metallicity = [M/H] (def = 0) + temperature = Kelvin (def = 5000) + gravity = log gravity (def = 4.0) + """ + teff_arr, z_arr, logg_arr = get_atmosphere_grid('nextgen') + return teff_arr, z_arr, logg_arr + def get_amesdusty_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): """ metallicity = [M/H] (def = 0) temperature = Kelvin (def = 5000) gravity = log gravity (def = 4.0) """ + sp = pysynphot.Icat('AMESdusty', temperature, metallicity, gravity) # Do some error checking @@ -236,6 +304,15 @@ def get_amesdusty_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=F return sp +def get_amesdusty_atmosphere_grid(): + """ + metallicity = [M/H] (def = 0) + temperature = Kelvin (def = 5000) + gravity = log gravity (def = 4.0) + """ + teff_arr, z_arr, logg_arr = get_atmosphere_grid('AMESdusty') + return teff_arr, z_arr, logg_arr + def get_phoenix_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): """ @@ -257,8 +334,8 @@ def get_phoenix_atmosphere(metallicity=0, temperature=5000, gravity=4, If true, rebins the atmospheres so that they are the same resolution as the Castelli+04 atmospheres. Default is False, which is often sufficient synthetic photometry in most cases. - """ + try: sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) except: @@ -280,6 +357,15 @@ def get_phoenix_atmosphere(metallicity=0, temperature=5000, gravity=4, return sp +def get_phoenix_atmosphere_grid(): + """ + Return atmosphere grid from the pysynphot + `PHOENIX atlas `_. + + """ + teff_arr, z_arr, logg_arr = get_atmosphere_grid('phoenix') + return teff_arr, z_arr, logg_arr + def get_cmfgenRot_atmosphere(metallicity=0, temperature=24000, gravity=4.3, rebin=True, verbose=False): """ metallicity = [M/H] (def = 0) @@ -288,6 +374,7 @@ def get_cmfgenRot_atmosphere(metallicity=0, temperature=24000, gravity=4.3, rebi rebin=True: pull from atmospheres at ck04model resolution. """ + # Take care of atmospheres outside the catalog boundaries logg_msg = 'Changing to logg={0:3.1f} for T={1:6.0f} logg={2:4.2f}' if gravity > 4.3: @@ -310,6 +397,21 @@ def get_cmfgenRot_atmosphere(metallicity=0, temperature=24000, gravity=4.3, rebi return sp +def get_cmfgenRot_atmosphere_grid(rebin=True): + """ + metallicity = [M/H] (def = 0) + temperature = Kelvin (def = 24000) + gravity = log gravity (def = 4.3) + + rebin=True: pull from atmospheres at ck04model resolution. + """ + if rebin: + teff_arr, z_arr, logg_arr = get_atmosphere_grid('cmfgen_rot_rebin') + else: + teff_arr, z_arr, logg_arr = get_atmosphere_grid('cmfgen_rot') + return teff_arr, z_arr, logg_arr + + def get_cmfgenRot_atmosphere_closest(metallicity=0, temperature=24000, gravity=4.3, rebin=True, verbose=False): """ @@ -386,6 +488,7 @@ def get_cmfgenRot_atmosphere_closest(metallicity=0, temperature=24000, gravity=4 return sp + def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=22500, gravity=3.98, rebin=True): """ metallicity = [M/H] (def = 0) @@ -394,6 +497,7 @@ def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=22500, gravity=3.98, r rebin=True: pull from atmospheres at ck04model resolution. """ + if rebin: sp = pysynphot.Icat('cmfgen_norot_rebin', temperature, metallicity, gravity) else: @@ -409,12 +513,27 @@ def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=22500, gravity=3.98, r return sp +def get_cmfgenNoRot_atmosphere_grid(rebin=True): + """ + metallicity = [M/H] (def = 0) + temperature = Kelvin (def = 24000) + gravity = log gravity (def = 4.3) + + rebin=True: pull from atmospheres at ck04model resolution. + """ + if rebin: + teff_arr, z_arr, logg_arr = get_atmosphere_grid('cmfgen_norot_rebin') + else: + teff_arr, z_arr, logg_arr = get_atmosphere_grid('cmfgen_norot') + return teff_arr, z_arr, logg_arr + def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=30000, gravity=4.14): """ metallicity = [M/H] (def = 0) temperature = Kelvin (def = 30000) gravity = log gravity (def = 4.14) """ + sp = pysynphot.Icat('cmfgenF15_noRot', temperature, metallicity, gravity) # Do some error checking @@ -427,6 +546,15 @@ def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=30000, gravity=4.14): return sp +def get_cmfgenNoRot_atmosphere_grid(): + """ + metallicity = [M/H] (def = 0) + temperature = Kelvin (def = 30000) + gravity = log gravity (def = 4.14) + """ + teff_arr, z_arr, logg_arr = get_atmosphere_grid('cmfgenF15_noRot') + return teff_arr, z_arr, logg_arr + def get_phoenixv16_atmosphere(metallicity=0, temperature=4000, gravity=4, rebin=True): """ Return PHOENIX v16 atmospheres from @@ -456,8 +584,8 @@ def get_phoenixv16_atmosphere(metallicity=0, temperature=4000, gravity=4, rebin= If true, rebins the atmospheres so that they are the same resolution as the Castelli+04 atmospheres. Default is False, which is often sufficient synthetic photometry in most cases. - """ + atm_model_name = 'phoenix_v16' if rebin == True: atm_model_name = 'phoenix_v16_rebin' @@ -485,6 +613,33 @@ def get_phoenixv16_atmosphere(metallicity=0, temperature=4000, gravity=4, rebin= return sp +def get_phoenixv16_atmosphere_grid(rebin=True): + """ + Return PHOENIX v16 atmosphere grid from + `Husser et al. 2013 `_. + + Models originally downloaded via `ftp `_. + Solar metallicity and [alpha/Fe] is used. + + Grid Range: + + * Teff: 2300 - 7000 K, steps of 100 K; 7000 - 12000 in steps of 200 K + * gravity: 0.0 - 6.0 cgs, steps of 0.5 + * [M/H]: -4.0 - 1.0 + + rebin: boolean + If true, rebins the atmospheres so that they are the same + resolution as the Castelli+04 atmospheres. Default is True + """ + + atm_model_name = 'phoenix_v16' + if rebin == True: + atm_model_name = 'phoenix_v16_rebin' + + teff_arr, z_arr, logg_arr = get_atmosphere_grid(atm_model_name) + return teff_arr, z_arr, logg_arr + + def get_BTSettl_2015_atmosphere(metallicity=0, temperature=2500, gravity=4, rebin=True): """ Return atmosphere from CIFIST2011_2015 grid @@ -542,6 +697,33 @@ def get_BTSettl_2015_atmosphere(metallicity=0, temperature=2500, gravity=4, rebi return sp +def get_BTSettl_2015_atmosphere_grid(rebin=True): + """ + Return atmosphere grid from CIFIST2011_2015 grid + (`Allard et al. 2012 `_, + `Baraffe et al. 2015 `_ ) + + Grid originally downloaded from `website `_. + + Grid Range: + + * Teff: 1200 - 7000 K + * gravity: 2.5 - 5.5 cgs + * [M/H] = 0 + + rebin: boolean + If true, rebins the atmospheres so that they are the same + resolution as the Castelli+04 atmospheres. Default is True + """ + if rebin == True: + atm_name = 'BTSettl_2015_rebin' + else: + atm_name = 'BTSettl_2015' + + teff_arr, z_arr, logg_arr = get_atmosphere_grid(atm_name) + return teff_arr, z_arr, logg_arr + + def get_BTSettl_atmosphere(metallicity=0, temperature=2500, gravity=4.5, rebin=True): """ Return atmosphere from CIFIST2011 grid @@ -674,6 +856,18 @@ def get_Meisner2023_atmosphere(metallicity=0, temperature=1000, gravity=4.5, reb return sp +def get_Meisner2023_atmosphere_grid(rebin=True): + """ + Return atmosphere grid from Meisner2023. + """ + if rebin == True: + atm_name = 'Meisner2023_rebin' + else: + atm_name = 'Meisner2023' + + teff_arr, z_arr, logg_arr = get_atmosphere_grid(atm_name) + return teff_arr, z_arr, logg_arr + def get_Phillips2020_atmosphere(metallicity=0, temperature=1000, gravity=4.5, rebin=True): """ Return atmosphere from Phillips et al., 2020 using ATMO model @@ -712,6 +906,76 @@ def get_Phillips2020_atmosphere(metallicity=0, temperature=1000, gravity=4.5, re return sp +def get_BTSettl_atmosphere_grid(rebin=True): + """ + Return atmosphere grid from CIFIST2011 grid + (`Allard et al. 2012 `_) + + Grid originally downloaded `here `_ + + Notes + ------ + Grid Range: + + * [M/H] = -2.5, -2.0, -1.5, -1.0, -0.5, 0, 0.5 + + Teff and gravity ranges depend on metallicity: + + [M/H] = -2.5 + + * Teff: 2600 - 4600 K + * gravity: 4.5 - 5.5 + + [M/H] = -2.0 + + * Teff: 2600 - 7000 + * gravity: 4.5 - 5.5 + + [M/H] = -1.5 + + * Teff: 2600 - 7000 + * gravity: 4.5 - 5.5 + + [M/H] = -1.0 + + * Teff: 2600 - 7000 + * gravity: Teff < 3200 --> 4.5 - 5.5; Teff > 3200 --> 2.5 - 5.5 + + [M/H] = -0.5 + + * Teff: 1000 -7000 + * gravity: Teff < 3000 --> 4.5 - 5.5; Teff > 3000 --> 3.0 - 6.0 + + [M/H] = 0 + + * Teff: 750 - 7000 + * gravity: Teff < 2500 --> 3.5 - 5.5; Teff > 2500 --> 0 - 5.5 + + [M/H] = 0.5 + + * Teff: 1000 - 5000 + * gravity: 3.5 - 5.0 + + + Alpha enhancement: + + * [M/H]= -0.0, +0.5 no anhancement + * [M/H]= -0.5 with [alpha/H]=+0.2 + * [M/H]= -1.0, -1.5, -2.0, -2.5 with [alpha/H]=+0.4 + + rebin: boolean + If true, rebins the atmospheres so that they are the same + resolution as the Castelli+04 atmospheres. Default is True. + """ + if rebin == True: + atm_name = 'BTSettl_rebin' + else: + atm_name = 'BTSettl' + + teff_arr, z_arr, logg_arr = get_atmosphere_grid(atm_name) + return teff_arr, z_arr, logg_arr + + def get_wdKoester_atmosphere(metallicity=0, temperature=20000, gravity=7): """ Return white dwarf atmospheres from @@ -733,6 +997,7 @@ def get_wdKoester_atmosphere(metallicity=0, temperature=20000, gravity=7): resolution as the Castelli+04 atmospheres. Default is False, which is often sufficient synthetic photometry in most cases. """ + sp = pysynphot.Icat('wdKoester', temperature, metallicity, gravity) # Do some error checking @@ -745,12 +1010,21 @@ def get_wdKoester_atmosphere(metallicity=0, temperature=20000, gravity=7): return sp +def get_wdKoester_atmosphere_grid(): + """ + Return white dwarf grid atmospheres from + `Koester et al. 2010 `_ + """ + teff_arr, z_arr, logg_arr = get_atmosphere_grid('wdKoester') + return teff_arr, z_arr, logg_arr + def get_atlas_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): """ Return atmosphere that is a linear merge of atlas ck04 model and phoenixV16. Only valid for temps between 5000 - 5500K, gravity from 0 = 5.0 """ + try: sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) except: @@ -772,6 +1046,15 @@ def get_atlas_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): return sp +def get_atlas_phoenix_atmosphere_grid(): + """ + Return atmosphere that is a linear merge of atlas ck04 model and phoenixV16. + + Only valid for temps between 5000 - 5500K, gravity from 0 = 5.0 + """ + teff_arr, z_arr, logg_arr = get_atmosphere_grid('merged_atlas_phoenix') + return teff_arr, z_arr, logg_arr + def get_BTSettl_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): """ Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model @@ -800,6 +1083,18 @@ def get_BTSettl_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): return sp + +def get_BTSettl_phoenix_atmosphere_grid(): + """ + Return atmosphere grid that is a linear merge of BTSettl_CITFITS2011_2015 model + and phoenixV16. + + Only valid for temps between 3200 - 3800K, gravity from 2.5 - 5.5 + """ + teff_arr, z_arr, logg_arr = get_atmosphere_grid('merged_BTSettl_phoenix') + return teff_arr, z_arr, logg_arr + + def get_BTSettl_meisner_atmosphere(metallicity=0, temperature=5250, gravity=4): """ Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model @@ -828,6 +1123,16 @@ def get_BTSettl_meisner_atmosphere(metallicity=0, temperature=5250, gravity=4): return sp +def get_BTSettl_meisner_atmosphere_grid(): + """ + Return atmosphere grid that is a linear merge of BTSettl_CITFITS2011_2015 model + and Meisner2023. + + Only valid for temps between 1000 - 1200K, gravity from 3.5 - 5.5 + """ + teff_arr, z_arr, logg_arr = get_atmosphere_grid('merged_BTSettl_meisner') + return teff_arr, z_arr, logg_arr + #---------------------------------------------------------------------# def get_merged_atmosphere(metallicity=0, temperature=20000, gravity=4.5, verbose=False, rebin=True): @@ -903,6 +1208,7 @@ def get_merged_atmosphere(metallicity=0, temperature=20000, gravity=4.5, verbose temperature ranges where we switch between model grids, to ensure a smooth transition. """ + # For T < 3800, atmosphere depends on metallicity + gravity. # If solar metallicity, use BTSettl 2015 grid. Only solar metallicity is # currently available here, so if non-solar metallicity, just stick with @@ -964,7 +1270,6 @@ def get_merged_atmosphere(metallicity=0, temperature=20000, gravity=4.5, verbose temperature=temperature, gravity=gravity, rebin=rebin) - # For T > 3800, no metallicity or gravity dependence if (temperature >= 3800) & (temperature < 5000): if verbose: @@ -1001,8 +1306,210 @@ def get_merged_atmosphere(metallicity=0, temperature=20000, gravity=4.5, verbose # gravity=gravity) +def get_merged_atmosphere_grid(rebin=True): + + # temp array, metallicity array, logg array + Meisner2023_atmosphere_arrs = np.array(get_Meisner2023_atmosphere_grid(rebin)) + + BTSettl_2015_atmosphere_arrs = np.array(get_BTSettl_2015_atmosphere_grid(rebin)) + + BTSettl_phoenix_atmosphere_arrs = np.array(get_BTSettl_phoenix_atmosphere_grid()) + + phoenixv16_atmosphere_arrs = np.array(get_phoenixv16_atmosphere_grid(rebin)) + atlas_phoenix_atmosphere_arrs = np.array(get_atlas_phoenix_atmosphere_grid()) + + castelli_atmosphere_arrs = np.array(get_castelli_atmosphere_grid()) + + Meisner2023_atmosphere_idxs = np.where(Meisner2023_atmosphere_arrs[0] <= 1200) + Meisner2023_atmosphere_arrs = Meisner2023_atmosphere_arrs[:,Meisner2023_atmosphere_idxs] + + BTSettl_2015_atmosphere_idxs = np.where((BTSettl_2015_atmosphere_arrs[0] > 1200) &\ + (BTSettl_2015_atmosphere_arrs[0] <= 3200) &\ + (BTSettl_2015_atmosphere_arrs[1] == 0) &\ + (BTSettl_2015_atmosphere_arrs[2] > 2.5)) + BTSettl_2015_atmosphere_arrs = BTSettl_2015_atmosphere_arrs[:,BTSettl_2015_atmosphere_idxs] + + BTSettl_phoenix_atmosphere_idxs = np.where((BTSettl_phoenix_atmosphere_arrs[0] >= 3200) &\ + (BTSettl_phoenix_atmosphere_arrs[0] < 3800) &\ + (BTSettl_phoenix_atmosphere_arrs[1] == 0) &\ + (BTSettl_phoenix_atmosphere_arrs[2] > 2.5)) + BTSettl_phoenix_atmosphere_arrs = BTSettl_phoenix_atmosphere_arrs[:,BTSettl_phoenix_atmosphere_idxs] + + phoenixv16_atmosphere_idxs = np.where(((phoenixv16_atmosphere_arrs[0] <= 3800) & (phoenixv16_atmosphere_arrs[1] == 0) & (phoenixv16_atmosphere_arrs[2] <= 2.5)) |\ + ((phoenixv16_atmosphere_arrs[0] <= 3800) & (phoenixv16_atmosphere_arrs[1] != 0)) |\ + ((phoenixv16_atmosphere_arrs[0] >= 3800) & (phoenixv16_atmosphere_arrs[0] < 5000))) + phoenixv16_atmosphere_arrs = phoenixv16_atmosphere_arrs[:,phoenixv16_atmosphere_idxs] + + atlas_phoenix_atmosphere_idxs = np.where((atlas_phoenix_atmosphere_arrs[0] >= 5000) & (atlas_phoenix_atmosphere_arrs[0] < 5500)) + atlas_phoenix_atmosphere_arrs = atlas_phoenix_atmosphere_arrs[:,atlas_phoenix_atmosphere_idxs] + + castelli_atmosphere_idxs = np.where(castelli_atmosphere_arrs[0] >= 5500) + castelli_atmosphere_arrs = castelli_atmosphere_arrs[:,castelli_atmosphere_idxs] + + super_tarr = np.concatenate((Meisner2023_atmosphere_arrs[0][0], BTSettl_2015_atmosphere_arrs[0][0], + BTSettl_phoenix_atmosphere_arrs[0][0], + phoenixv16_atmosphere_arrs[0][0], atlas_phoenix_atmosphere_arrs[0][0], + castelli_atmosphere_arrs[0][0])) + + super_zarr = np.concatenate((Meisner2023_atmosphere_arrs[1][0], BTSettl_2015_atmosphere_arrs[1][0], + BTSettl_phoenix_atmosphere_arrs[1][0], + phoenixv16_atmosphere_arrs[1][0], atlas_phoenix_atmosphere_arrs[1][0], + castelli_atmosphere_arrs[1][0])) + + super_loggarr = np.concatenate((Meisner2023_atmosphere_arrs[2][0], BTSettl_2015_atmosphere_arrs[2][0], + BTSettl_phoenix_atmosphere_arrs[2][0], + phoenixv16_atmosphere_arrs[2][0], atlas_phoenix_atmosphere_arrs[2][0], + castelli_atmosphere_arrs[2][0])) + + return super_tarr, super_zarr, super_loggarr + +def get_merged_atmosphere_w_bb_supplement(metallicity=0, temperature=20000, gravity=4.5, verbose=False, + rebin=True): + """ + Return a stellar atmosphere from a suite of different model grids, + depending on the input temperature, (all values in K). + IF OUTSIDE SUPPORTED GRID, WILL RETURN BB ATMOSPHERE + + Parameters + ---------- + metallicity: float + The stellar metallicity, in terms of [Z] + + temperature: float + The stellar temperature, in units of K + + gravity: float + The stellar gravity, in cgs units + + rebin: boolean + If true, rebins the atmospheres so that they are the same + resolution as the Castelli+04 atmospheres. Default is False, + which is often sufficient synthetic photometry in most cases. + + verbose: boolean + True for verbose output + + Notes + ----- + The underlying stellar model grid used changes as a function of + stellar temperature (in K): + + * T > 20,000: ATLAS + * 5500 <= T < 20,000: ATLAS + * 5000 <= T < 5500: ATLAS/PHOENIXv16 merge + * 3800 <= T < 5000: PHOENIXv16 + + For T < 3800, there is an additional gravity and metallicity + dependence: + + If T < 3800 and [M/H] = 0: + + * T < 3800, logg < 2.5: PHOENIX v16 + * 3200 <= T < 3800, logg > 2.5: BTSettl_CIFITS2011_2015/PHOENIXV16 merge + * 1200 < T <= 3200, logg > 2.5: BTSettl_CIFITS2011_2015 + * 1000 <= T <= 1200, logg >= 2.5: Meisner2023 + * 250 <= T < 1000: Meisner2023 + + Otherwise, if T < 3800 and [M/H] != 0: + + * T < 3800: PHOENIX v16 + + References: + + * ATLAS: ATLAS9 models (`Castelli & Kurucz 2004 `_) + * PHOENIXv16 (`Husser et al. 2013 `_) + * BTSettl_CIFITS2011_2015: Baraffee+15, Allard+ (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011_2015/SPECTRA/) + * Meisner2023: ATMO 1D models (`Meisner et al. 2023 `_) + + LTE WARNING: + + The ATLAS atmospheres are calculated with LTE, and so they + are less accurate when non-LTE conditions apply (e.g. T > 20,000 + K). Ultimately we'd like to add a non-LTE atmosphere grid for + the hottest stars in the future. + + HOW BOUNDARIES BETWEEN MODELS ARE TREATED: + + At the boundary between two models grids a temperature range is defined + where the resulting atmosphere is a weighted average between the two + grids. Near one boundary one model + is weighted more heavily, while at the other boundary the other + model is weighted more heavily. These are calculated in the + temperature ranges where we switch between model grids, to + ensure a smooth transition. + """ + + if (gravity >= 9.8): + if verbose: + print('BB atmosphere') + return get_bb_atmosphere(temperature=temperature, + metallicity=metallicity, + gravity=gravity, + verbose=verbose) + if (temperature < 4.6e3) & (gravity >= 6.5): + if verbose: + print('BB atmosphere') + return get_bb_atmosphere(temperature=temperature, + metallicity=metallicity, + gravity=gravity, + verbose=verbose) + + if (temperature < 3.5e3) & (gravity < 6.5) & (gravity > 6): + if verbose: + print('BB atmosphere') + return get_bb_atmosphere(temperature=temperature, + metallicity=metallicity, + gravity=gravity, + verbose=verbose) + + if gravity > 6: + if verbose: + print('WD or BB atmosphere') + return get_wd_atmosphere(metallicity=metallicity, + temperature=temperature, + gravity=gravity, + verbose=verbose) + + return get_merged_atmosphere(metallicity=metallicity, + temperature=temperature, + gravity=gravity, + verbose=verbose, + rebin=rebin) + +def get_merged_atmosphere_w_bb_supplement_grid(bb_supplement_tarr='default', bb_supplement_zarr='default', bb_supplement_loggarr='default', rebin=True): + """ + Return the atmosphere parameter grid used by + get_merged_atmosphere_w_bb_supplement. + + This is the standard merged atmosphere grid plus the white dwarf grid and + blackbody supplement points for high-gravity regions. + """ + super_tarr, super_zarr, super_loggarr = get_merged_atmosphere_grid(rebin=rebin) + + wd_tarr, wd_zarr, wd_loggarr = get_wdKoester_atmosphere_grid() + super_tarr = np.concatenate((super_tarr, wd_tarr)) + super_zarr = np.concatenate((super_zarr, wd_zarr)) + super_loggarr = np.concatenate((super_loggarr, wd_loggarr)) + + if bb_supplement_tarr == 'default': + X_highg, Y_highg = np.meshgrid(np.logspace(np.log10(2e3), np.log10(2e4), 35), + np.linspace(9.8, 11.6, 8)) + X_cool_highg, Y_cool_highg = np.meshgrid(np.logspace(np.log10(2e3), np.log10(4.6e3), 20), + np.linspace(6.5, 9.7, 10)) + X_cool_midg, Y_cool_midg = np.meshgrid(np.logspace(np.log10(2e3), np.log10(3.5e3), 15), + np.linspace(6.1, 6.4, 4)) + + bb_supplement_tarr = np.concatenate((X_highg.ravel(), X_cool_highg.ravel(), X_cool_midg.ravel())) + bb_supplement_loggarr = np.concatenate((Y_highg.ravel(), Y_cool_highg.ravel(), Y_cool_midg.ravel())) + bb_supplement_zarr = np.zeros(len(bb_supplement_tarr)) + + super_tarr = np.concatenate((super_tarr, bb_supplement_tarr)) + super_zarr = np.concatenate((super_zarr, bb_supplement_zarr)) + super_loggarr = np.concatenate((super_loggarr, bb_supplement_loggarr)) + return super_tarr, super_zarr, super_loggarr + def get_wd_atmosphere(metallicity=0, temperature=20000, gravity=4, verbose=False): """ Return the white dwarf atmosphere from diff --git a/spisea/atmospheres_BACKUP_22350.py b/spisea/atmospheres_BACKUP_22350.py deleted file mode 100755 index e3d2233c..00000000 --- a/spisea/atmospheres_BACKUP_22350.py +++ /dev/null @@ -1,2524 +0,0 @@ -import logging -import numpy as np -import pysynphot -import os -import glob -from astropy.io import fits -from astropy.table import Table, Column -import pysynphot -import time -import pdb -import warnings - -log = logging.getLogger('atmospheres') - -def get_atmosphere_bounds(model_dir, metallicity=0, temperature=20000, gravity=4, verbose=False): - """ - Given atmosphere model, get temperature and gravity bounds - """ - # Open catalog fits file and break out row indices - catalog = Table.read('{0}/grid/{1}/catalog.fits'.format(os.environ['PYSYN_CDBS'], model_dir)) - - teff_arr = [] - z_arr = [] - logg_arr = [] - for cur_row_index in range(len(catalog)): - index = catalog['INDEX'][cur_row_index] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Filter by metallicity. Will chose the closest metallicity to desired input - metal_list = np.unique(np.array(z_arr)) - metal_idx = np.argmin(np.abs(metal_list - metallicity)) - metallicity_new = metal_list[metal_idx] - - z_filt = np.where(z_arr == metal_list[metal_idx]) - teff_arr = teff_arr[z_filt] - logg_arr = logg_arr[z_filt] - - # # Now find the closest atmosphere in parameter space to - # # the one we want. We'll find the match with the lowest - # # fractional difference - # teff_diff = (teff_arr - temperature) / temperature - # logg_diff = (logg_arr - gravity) / gravity - # - # diff_tot = abs(teff_diff) + abs(logg_diff) - # idx_f = np.argmin(diff_tot) - # - # temperature_new = teff_arr[idx_f] - # gravity_new = logg_arr[idx_f] - - # First check if temperature within bounds - temperature_new = temperature - if temperature > np.max(teff_arr): - temperature_new = np.max(teff_arr) - if temperature < np.min(teff_arr): - temperature_new = np.min(teff_arr) - - # If temperature within bounds, then check if metallicity within bounds - teff_diff = np.abs(teff_arr - temperature) - sorted_min_diffs = np.unique(teff_diff) - - ## Find two closest temperatures - teff_close_1 = teff_arr[np.where(teff_diff == sorted_min_diffs[0])[0][0]] - teff_close_2 = teff_arr[np.where(teff_diff == sorted_min_diffs[1])[0][0]] - - logg_arr_1 = logg_arr[np.where(teff_arr == teff_close_1)] - logg_arr_2 = logg_arr[np.where(teff_arr == teff_close_2)] - - ## Switch to most conservative bound of logg out of two closest temps - gravity_new = gravity - if gravity > np.min([np.max(logg_arr_1), np.max(logg_arr_2)]): - gravity_new = np.min([np.max(logg_arr_1), np.max(logg_arr_2)]) - if gravity < np.max([np.min(logg_arr_1), np.min(logg_arr_2)]): - gravity_new = np.max([np.min(logg_arr_1), np.min(logg_arr_2)]) - - if verbose: - # Print out changes, if any - if temperature_new != temperature: - teff_msg = 'Changing to T={0:6.0f} for met={1:4.2f} T={2:6.0f} logg={3:4.2f}' - print( teff_msg.format(temperature_new, metallicity, temperature, gravity)) - - if gravity_new != gravity: - logg_msg = 'Changing to logg={0:4.2f} for met={1:4.2f} T={2:6.0f} logg={3:4.2f}' - print( logg_msg.format(gravity_new, metallicity, temperature, gravity)) - - if metallicity_new != metallicity: - logg_msg = 'Changing to met={0:4.2f} for met={1:4.2f} T={2:6.0f} logg={3:4.2f}' - print( logg_msg.format(metallicity_new, metallicity, temperature, gravity)) - - return (temperature_new, gravity_new, metallicity_new) - -def get_kurucz_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmosphere from the Kurucz pysnphot grid - (`Kurucz 1993 `_). - - Grid Range: - - * Teff: 3000 - 50000 K - * gravity: 0 - 5 cgs - * metallicity: -5.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - Always false for this particular function - """ - try: - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('k93models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Kurucz 1993 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_castelli_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmospheres from the pysynphot ATLAS9 atlas - (`Castelli & Kurucz 2004 `_). - - Grid Range: - - * Teff: 3500 - 50000 K - * gravity: 0 - 5.0 cgs - * [M/H]: -2.5 - 0.2 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('ck04models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Castelli and Kurucz 2004 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_nextgen_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - try: - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('nextgen', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find NextGen atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_amesdusty_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - sp = pysynphot.Icat('AMESdusty', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find AMESdusty Allard+ 2000 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenix_atmosphere(metallicity=0, temperature=5000, gravity=4, - rebin=False): - """ - Return atmosphere from the pysynphot - `PHOENIX atlas `_. - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - try: - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIX BT-Settl (Allard+ 2011 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere(metallicity=0, temperature=24000, gravity=4.3, rebin=True, verbose=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - # Take care of atmospheres outside the catalog boundaries - logg_msg = 'Changing to logg={0:3.1f} for T={1:6.0f} logg={2:4.2f}' - if gravity > 4.3: - if verbose: - print( logg_msg.format(4.3, temperature, gravity)) - gravity = 4.3 - - if rebin: - sp = pysynphot.Icat('cmfgen_rot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_rot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere_closest(metallicity=0, temperature=24000, gravity=4.3, rebin=True, - verbose=False): - """ - For a given stellar atmosphere, get extract the closest possible match in - Teff/logg space. Note that this is different from the normal routine - which interpolates along the input grid to get final spectrum. We can't - do this here because the Fierro+15 atmosphere grid is so sparse - - rebin=True: pull from atmospheres at ck04model resolution. - - If verbose, print out the parameters of the match - """ - # Set up the proper root directory - if rebin == True: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot_rebin/' - else: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot/' - - # Read in catalog, extract atmosphere info - cat = Table.read('{0}/catalog.fits'.format(root_dir), format='fits') - teff_arr = [] - z_arr = [] - logg_arr = [] - for ii in range(len(cat)): - index = cat['INDEX'][ii] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Now find the closest atmosphere in parameter space to - # the one we want. We'll find the match with the lowest - # fractional difference - teff_diff = (teff_arr - temperature) / temperature - logg_diff = (logg_arr - gravity) / gravity - - diff_tot = abs(teff_diff) + abs(logg_diff) - idx_f = np.where(diff_tot == min(diff_tot))[0][0] - - # Extract the filename of the best-match model and read as - # pysynphot object - infile = cat[idx_f]['FILENAME'].split('.') - spec = Table.read('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Now, the CMFGEN atmospheres assume a distance of 1 kpc, while the the - # ATLAS models are in FLAM at the surface. So, we need to multiply the - # CMFGEN atmospheres by (1000/R)**2. in order to convert to FLAM on surface. - # We'll calculate radius from Teff and logL, which is given in the Table_*.txt file - t = Table.read('{0}/Table_rot.txt'.format(root_dir), format='ascii') - tmp = np.where(t['col1'] == infile[0]) - - lum = t['col3'][tmp] * (3.839*10**33) # cgs - sigma = 5.6704 * 10**-5 # cgs - teff = teff_arr[idx_f] # cgs - - radius = np.sqrt( lum / (4.0 * np.pi * teff**4. * sigma) ) # in cm - radius /= 3.08*10**18 # in pc - - - # Make the pysynphot spectrum - w = spec['Wavelength'] - f = spec['Flux'] * (1000 / radius)**2. - sp = pysynphot.ArraySpectrum(w,f) - - #sp = pysynphot.FileSpectrum('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Print out parameters of match, if desired - if verbose: - print('Teff match: Input: {0}, Output: {1}'.format(temperature, teff_arr[idx_f])) - print('logg match: Input: {0}, Output: {1}'.format(gravity, logg_arr[idx_f])) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=22500, gravity=3.98, rebin=True): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - if rebin: - sp = pysynphot.Icat('cmfgen_norot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_norot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=30000, gravity=4.14): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 30000) - gravity = log gravity (def = 4.14) - """ - sp = pysynphot.Icat('cmfgenF15_noRot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN non-rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenixv16_atmosphere(metallicity=0, temperature=4000, gravity=4, rebin=True): - """ - Return PHOENIX v16 atmospheres from - `Husser et al. 2013 `_. - - Models originally downloaded via `ftp `_. - Solar metallicity and [alpha/Fe] is used. - - Grid Range: - - * Teff: 2300 - 7000 K, steps of 100 K; 7000 - 12000 in steps of 200 K - * gravity: 0.0 - 6.0 cgs, steps of 0.5 - * [M/H]: -4.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - atm_model_name = 'phoenix_v16' - if rebin == True: - atm_model_name = 'phoenix_v16_rebin' - - - # Extract atmosphere. If that fails, then check bounds and try again - try: - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds(atm_model_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIXv16 (Husser+13) atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_2015_atmosphere(metallicity=0, temperature=2500, gravity=4, rebin=True): - """ - Return atmosphere from CIFIST2011_2015 grid - (`Allard et al. 2012 `_, - `Baraffe et al. 2015 `_ ) - - Grid originally downloaded from `website `_. - - Grid Range: - - * Teff: 1200 - 7000 K - * gravity: 2.5 - 5.5 cgs - * [M/H] = 0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - if rebin == True: - atm_name = 'BTSettl_2015_rebin' - else: - atm_name = 'BTSettl_2015' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) -<<<<<<< HEAD - #print(dir(obj)) - - -======= - - ->>>>>>> upstream/dev - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl_2015 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_atmosphere(metallicity=0, temperature=2500, gravity=4.5, rebin=True): - """ - Return atmosphere from CIFIST2011 grid - (`Allard et al. 2012 `_) - - Grid originally downloaded `here `_ - - Notes - ------ - Grid Range: - - * [M/H] = -2.5, -2.0, -1.5, -1.0, -0.5, 0, 0.5 - - Teff and gravity ranges depend on metallicity: - - [M/H] = -2.5 - - * Teff: 2600 - 4600 K - * gravity: 4.5 - 5.5 - - [M/H] = -2.0 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.5 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.0 - - * Teff: 2600 - 7000 - * gravity: Teff < 3200 --> 4.5 - 5.5; Teff > 3200 --> 2.5 - 5.5 - - [M/H] = -0.5 - - * Teff: 1000 -7000 - * gravity: Teff < 3000 --> 4.5 - 5.5; Teff > 3000 --> 3.0 - 6.0 - - [M/H] = 0 - - * Teff: 750 - 7000 - * gravity: Teff < 2500 --> 3.5 - 5.5; Teff > 2500 --> 0 - 5.5 - - [M/H] = 0.5 - - * Teff: 1000 - 5000 - * gravity: 3.5 - 5.0 - - - Alpha enhancement: - - * [M/H]= -0.0, +0.5 no anhancement - * [M/H]= -0.5 with [alpha/H]=+0.2 - * [M/H]= -1.0, -1.5, -2.0, -2.5 with [alpha/H]=+0.4 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - **PRINT STATEMENTS TO DEBUG - **check get_atmosphere_bounds - **comment out try/except clause and check break - """ - if rebin == True: - atm_name = 'BTSettl_rebin' - else: - atm_name = 'BTSettl' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) -<<<<<<< HEAD - -def get_Meisner2023_atmosphere(metallicity=0, temperature=1000, gravity=4.5, rebin=True): - """ - Return atmosphere from Meisner2023 grid - (`Meisner et al. 2023 `_) - - Grid originally downloaded `here `_ - - Grid range: - * Teff = 250 - 1200 K - * gravity: 2.5 - 5.5 cgs (in steps of 0.5) - * [M/H] = -1.0, -0.5, 0, +0, +0.3 - - """ - if rebin == True: - atm_name = 'Meisner2023_rebin' - else: - atm_name = 'Meisner2023' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) -======= - ->>>>>>> upstream/dev - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Meisner2023 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_Phillips2020_atmosphere(metallicity=0, temperature=1000, gravity=4.5, rebin=True): - """ - Return atmosphere from Phillips et al., 2020 using ATMO model - (`Phillips et al. 2020 `_) - - Grid originally downloaded `here `_ - - Grid Range: - * Teff: 200 - 3000 K - * gravity: 2.5 - 5.5 cgs - * [M/H] = 0 - """ - if rebin == True: - atm_name = 'Phillips2020_rebin' - else: - atm_name = 'Phillips2020' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Phillips2020 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_wdKoester_atmosphere(metallicity=0, temperature=20000, gravity=7): - """ - Return white dwarf atmospheres from - `Koester et al. 2010 `_ - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - sp = pysynphot.Icat('wdKoester', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find WD Koester (Koester+ 2010 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_atlas_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of atlas ck04 model and phoenixV16. - - Only valid for temps between 5000 - 5500K, gravity from 0 = 5.0 - """ - try: - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('merged_atlas_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model - and phoenixV16. - - Only valid for temps between 3200 - 3800K, gravity from 2.5 - 5.5 - """ - try: - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('merged_BTSettl_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_meisner_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model - and Meisner2023. - - Only valid for temps between 1000 - 1200K, gravity from 3.5 - 5.5 - """ - try: - sp = pysynphot.Icat('merged_BTSettl_meisner', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('merged_BTSettl_meisner', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_BTSettl_meisner', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl-Meisner merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -#---------------------------------------------------------------------# -def get_merged_atmosphere(metallicity=0, temperature=20000, gravity=4.5, verbose=False, - rebin=True): - """ - Return a stellar atmosphere from a suite of different model grids, - depending on the input temperature, (all values in K). - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - - Notes - ----- - The underlying stellar model grid used changes as a function of - stellar temperature (in K): - - * T > 20,000: ATLAS - * 5500 <= T < 20,000: ATLAS - * 5000 <= T < 5500: ATLAS/PHOENIXv16 merge - * 3800 <= T < 5000: PHOENIXv16 - - For T < 3800, there is an additional gravity and metallicity - dependence: - - If T < 3800 and [M/H] = 0: - - * T < 3800, logg < 2.5: PHOENIX v16 - * 3200 <= T < 3800, logg > 2.5: BTSettl_CIFITS2011_2015/PHOENIXV16 merge - * 3200 < T <= 1200, logg > 2.5: BTSettl_CIFITS2011_2015 - * 1200 < T <= 1000, logg >= 3.5: BTSettl_CIFITS2011_2015/Meisner2023 merge - * 1000 < T <= 250, logg > 2.5: Meisner2023 - - Otherwise, if T < 3800 and [M/H] != 0: - - * T < 3800: PHOENIX v16 - - References: - - * ATLAS: ATLAS9 models (`Castelli & Kurucz 2004 `_) - * PHOENIXv16 (`Husser et al. 2013 `_) - * BTSettl_CIFITS2011_2015: Baraffee+15, Allard+ (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011_2015/SPECTRA/) - * Meisner2023: ATMO 1D models (`Meisner et al. 2023 `_) - - LTE WARNING: - - The ATLAS atmospheres are calculated with LTE, and so they - are less accurate when non-LTE conditions apply (e.g. T > 20,000 - K). Ultimately we'd like to add a non-LTE atmosphere grid for - the hottest stars in the future. - - HOW BOUNDARIES BETWEEN MODELS ARE TREATED: - - At the boundary between two models grids a temperature range is defined - where the resulting atmosphere is a weighted average between the two - grids. Near one boundary one model - is weighted more heavily, while at the other boundary the other - model is weighted more heavily. These are calculated in the - temperature ranges where we switch between model grids, to - ensure a smooth transition. - """ - # For T < 3800, atmosphere depends on metallicity + gravity. - # If solar metallicity, use BTSettl 2015 grid. Only solar metallicity is - # currently available here, so if non-solar metallicity, just stick with - # the Phoenix grid - if (temperature < 1000): - if verbose: - print( 'Meisner2023 atmosphere') - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 1200) & (temperature >= 1000): - if (gravity >= 3.5): - if verbose: - print( 'BTSettl/Meisner2023 merged atmosphere') - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - if (gravity < 3.5) & (gravity >=2.5): - if verbose: - print( 'Meisner2023 atmosphere') - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 3800) & (metallicity == 0): - # High gravity are in BTSettl regime - if (temperature <= 3200) & (gravity > 2.5): - if verbose: - print( 'BTSettl_2015 atmosphere') - return get_BTSettl_2015_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 3200) & (temperature < 3800) & (gravity > 2.5): - if verbose: - print( 'BTSettl/Phoenixv16 merged atmosphere') - return get_BTSettl_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - # Low gravity is PHOENIX regime - if gravity <= 2.5: - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 3800) & (metallicity != 0): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - # For T > 3800, no metallicity or gravity dependence - if (temperature >= 3800) & (temperature < 5000): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 5000) & (temperature < 5500): - if verbose: - print( 'ATLAS/Phoenix merged atmosphere') - return get_atlas_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if (temperature >= 5500) & (temperature < 20000): - if verbose: - print( 'ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if temperature >= 20000: - if verbose: - print( 'Still ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - #print('CMFGEN') - #return get_cmfgenRot_atmosphere_closest(metallicity=metallicity, - # temperature=temperature, - # gravity=gravity) - - - - -def get_wd_atmosphere(metallicity=0, temperature=20000, gravity=4, verbose=False): - """ - Return the white dwarf atmosphere from - `Koester et al. 2010 `_. - If desired parameters are - outside of grid, return a blackbody spectrum instead - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - if verbose: - print('wdKoester atmosphere') - - return get_wdKoester_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - except pysynphot.exceptions.ParameterOutOfBounds: - # Use a black-body atmosphere. - bbspec = get_bb_atmosphere(temperature=temperature, verbose=verbose) - return bbspec - - -def get_bd_atmosphere(metallicity=0, temperature=1000, gravity=4, verbose=False): - """ - Return the brown dwarf atmosphere from - `Meisner et al. 2023 `_. - If desired parameters are - outside of grid, return a blackbody spectrum instead - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - if verbose: - print('Meisner2023 atmosphere') - - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - except pysynphot.exceptions.ParameterOutOfBounds: - # Use a black-body atmosphere - bbspec = get_bb_atmosphere(temperature=temperature, verbose=verbose) - return bbspec - -def get_bb_atmosphere(metallicity=None, temperature=20_000, gravity=None, - verbose=False, rebin=None, - wave_min=500, wave_max=50_000, wave_num=20_000): - """ - Return a blackbody spectrum - - Parameters - ---------- - temperature: float, default=20_000 - The stellar temperature, in units of K - wave_min: float, default=500 - Sets the minimum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_max: float, default=50_000 - Sets the maximum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_num: int, default=20_000 - Sets the number of wavelength points in the wavelength range - Note: the wavelength range is evenly spaced in log space - """ - if ((metallicity is not None) or (gravity is not None) or - (rebin is not None)): - warnings.warn( - 'Only `temperature` keyword is used for black-body atmosphere' - ) - - if verbose: - print('Black-body atmosphere') - - # Modify pysynphot's default waveset to specified bounds - pysynphot.refs.set_default_waveset( - minwave=wave_min, maxwave=wave_max, num=wave_num - ) - - # Get black-body atmosphere for specified temperature from pysynphot - bbspec = pysynphot.spectrum.BlackBody(temperature) - - # pysynphot `BlackBody` generates spectrum in `photlam`, need in `flam` - bbspec.convert('flam') - - # `BlackBody` spectrum is normalized to solar radius star at 1 kiloparsec. - # Need to remove this normalization for SPISEA by multiplying bbspec - # by (1000 * 1 parsec / 1 Rsun)**2 = (1000 * 3.08e18 cm / 6.957e10 cm)**2 - bbspec *= (1000 * 3.086e18 / 6.957e10)**2 - - return bbspec - - -#--------------------------------------# -# Atmosphere formatting functions -#--------------------------------------# - -def download_CMFGEN_atmospheres(Table_rot, Table_norot): - """ - Downloads CMFGEN models from - https://sites.google.com/site/fluxesandcontinuum/home; - these contain continuum as well as lines. - - Table_rot, Table_norot are tables with the file prefixes - and model atmosphere parameters, taken by hand from the - Fierro+15 paper - - Website addresses are hardcoded - - Puts downloaded models in the current working directory. - """ - print( 'WARNING: THIS DOES NOT COMPLETELY WORK') - print( '**********************') - t_rot = Table.read(Table_rot, format='ascii') - t_norot = Table.read(Table_norot, format='ascii') - - tables = [t_rot, t_norot] - filenames = [t_rot['col1'], t_norot['col1']] - - # Hardcoded list of webiste addresses - web_base1 = 'https://sites.google.com/site/fluxesandcontinuum/home/' - web_base2 = 'https://sites.google.com/site/modelsobmassivestars/' - web = [web_base1+'009-solar-masses/',web_base1+'012-solar-masses/', - web_base1+'015-solar-masses/',web_base1+'020-solar-masses/', - web_base1+'025-solar-masses/',web_base2+'009-solar-masses-tracks/', - web_base2+'040-solar-masses/',web_base2+'060-solar-masses/', - web_base1+'085-solar-masses/',web_base1+'120-solar-masses/'] - # Array of masses that matches the website addresses - mass_arr = np.array([9.,12.,15.,20.,25.,32.,40.,60.,85.,120.]) - - # Loop through rotating and unrotating case. First loop is rot, second unrot - for i in range(2): - # Extract masses from filenames - masses = [] - for j in filenames[i]: - tmp = j.split('m') - mass = float(tmp[1][:-1]) - masses.append(mass) - - # Download the models webpage by webpage. A bit tricky because masses - # change slightly within a particular website. THIS IS WHAT FAILS - for j in range(len(web)): - if j == 0: - good = np.where( (masses <= mass_arr[j]) ) - else: - g = j - 1 - good = np.where( (masses <= mass_arr[j]) & - (masses > mass_arr[g]) ) - # Use wget command to pull down the files, and unzip them - for k in good[0]: - full = web[j]+'{1:s}.flx.zip'.format(mass_arr[j],filenames[i][k]) - os.system('wget ' + full) - os.system('unzip '+ filenames[i][k] + '.flx.zip') - - return - -def organize_CMFGEN_atmospheres(path_to_dir): - """ - Organize CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html) - into rot and noRot directories - - path_to_dir is from current working directory to directory - containing the downloaded models. Assumed that models - and tables describing parameters are in this directory. - - Tables describing parameters MUST be named Table_rot.txt, - Table_noRot.txt. Made by hand from Tables 3, 4 in Fierro+15. - These are located in same original directory as atmosphere files - - Will separate files into 2 subdirectories, one rotating and - the other non-rotating - - *Can't have any other files starting with "t" in model directory to start!* - """ - # First, record current working directory to return to later - start_dir = os.getcwd() - - # Enter atmosphere directory, collect rotating and non-rotating - # file names (assumed to all start with "t") - os.chdir(path_to_dir) - rot_models = glob.glob("t*r.flx*") - noRot_models = glob.glob("t*n.flx*") - - # Separate into different subdirectories - if os.path.exists('cmfgenF15_rot'): - pass - else: - os.mkdir('cmfgenF15_rot') - os.mkdir('cmfgenF15_noRot') - - for mod in rot_models: - cmd = 'mv {0:s} cmfgenF15_rot'.format(mod) - os.system(cmd) - - for mod in noRot_models: - cmd = 'mv {0:s} cmfgenF15_noRot'.format(mod) - os.system(cmd) - - # Also move Tables with model parameters into correct directory - os.system('mv Table_rot.txt cmfgenF15_rot') - os.system('mv Table_noRot.txt cmfgenF15_noRot') - - # Return to original directory - os.chdir(start_dir) - - return - -def make_CMFGEN_catalog(path_to_dir): - """ - Create cdbs catalog.fits of CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html). - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to directory - containing the rotating or non-rotating models (i.e. cmfgenF15_rot). Also, - needs to be a Table*.txt file which contains the parameters for all of the - original models, since params in filename are not precise enough - - Will create catalog.fits file in atmosphere directory with - description of each model - - *Can't have any other files starting with "t" in model directory to start!* - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere - # Note: can't rely on filename for this because not precise enough!! - - #---------OLD: GETTING PARAMS FROM FILENAME-------# - # Collect file names (assumed to all start with "t") - #files = glob.glob("t*") - #for name in files: - # tmp = name.split('l') - # temp = float(tmp[0][1:]) * 100.0 # In kelvin - - # lumtmp = tmp[1].split('_') - # lum = float(lumtmp[0][:-5]) * 1000.0 # In L_sun - - # mass = float(lumtmp[0][5:-1]) # In M_sun - - # Need to calculate log g from T and L (cgs) - # lum_sun = 3.846 * 10**33 # erg/s - # M_sun = 2 * 10**33 # g - # G_si = 6.67 * 10**(-8) # cgs - # sigma_si = 5.67 * 10**(-5) # cgs - - # g = (G_si * mass * M_sun * 4 * np.pi * sigma_si * temp**4) / \ - # (lum * lum_sun) - # logg = np.log10(g) - #---------------------------------------------------# - - # Read table with atmosphere params - table = glob.glob('Table_*') - t = Table.read(table[0], format = 'ascii') - names = t['col1'] - temps = t['col2'] - logg = t['col4'] - - # Create catalog.fits file - index_str = [] - name_str = [] - for i in range(len(names)): - index = '{0:5.0f},0.0,{1:3.2f}'.format(temps[i], logg[i]) - - #---NOTE: THE FOLLOWING DEPENDS ON FINAL LOCATION OF CATALOG FILE---# - #path = path_to_dir + '/' + names[i] - path = names[i] + '.fits[Flux]' - - index_str.append(index) - name_str.append(path) - - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits') - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def cdbs_cmfgen(path_to_dir, path_to_cdbs_dir): - """ - Code to put cmfgen models into cdbs format and adds proper unit keyword in - fits header. Save as fits file - - path_to_dir goes from current directory to cmfgen_rot or cmfgen_norot - directory with the *.flx models. Note that these files have already been - organized using organize_CMFGEN_atmospheres code. - - path_to_cdbs_dir goes from current directory to cdbs/grid/cmfgen_rot or - cmfgen_norot directory. Will copy new fits files to this directory. - This directory must already exist! - """ - # Save starting directory for later, move into path_to_dir directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('*.flx') - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - # Open file, extract useful info - t = Table.read(i, format='ascii') - wave = t['col1'] - flux = t['col2'] # Flux is already in erg/cm^2/s/A - - # Need to eliminate duplicate entries (pysynphot crashes) - unique = np.unique(wave, return_index=True) - wave = wave[unique[1]] - flux = flux[unique[1]] - - # Make fits table from individual columns. - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - #Adding unit keywords - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - prihdu = fits.PrimaryHDU() - - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i[:-4]+'.fits', overwrite=True) - - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Return to original directory, copy over new .fits files to cdbs directory - os.chdir(start_dir) - cmd = 'mv {0:s}/*.fits {1:s}'.format(path_to_dir, path_to_cdbs_dir) - os.system(cmd) - - return - -def rebin_cmfgen(cdbs_path, rot=True): - """ - Rebin cmfgen_rot and cmfgen_norot models to atlas ck04 resolution; - this makes spectrophotometry MUCH faster - - cdbs_path: path to cdbs directory - rot=True for rotating models (cmfgen_rot), False for non-rotating models - - makes new directory in cdbs/grid: cmfgen_rot_rebin or cmfgen_norot_rebin - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing cmfgen model; we will steal the header. - # Also define paths to new rebin directories - if rot == True: - tmp = cdbs_path+'/grid/cmfgen_rot/t0200l0008m009r.fits' - path = cdbs_path+'/grid/cmfgen_rot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_rot/' - else: - tmp = cdbs_path+'/grid/cmfgen_norot/t0200l0007m009n.fits' - path = cdbs_path+'/grid/cmfgen_norot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_norot/' - - cmfgen_hdu = fits.open(tmp) - header0 = cmfgen_hdu[0].header - # Create rebin directories if they don't already exist. Copy over - # catalog.fits file from original directory (will be the same) - if not os.path.exists(path): - os.mkdir(path) - cmd = 'cp {0:s}catalog.fits {1:s}'.format(orig_path, path) - os.system(cmd) - - # Read in the catalog.fits file - cat = fits.getdata(orig_path + 'catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - # First column in new files will be for [atlas] wavelength - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - - # For each catalog.fits entry, read the unbinned spectrum and rebin to - # the atlas resolution. Make a new fits file in rebin directory - count = 0 - for ff in range(len(files_all)): - count += 1 - # Extract the temp, Z, logg - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - grav = float(vals[2]) - - # Fetch the spectrum - if rot == True: - sp = pysynphot.Icat('cmfgen_rot', temp, metal, grav) - else: - sp = pysynphot.Icat('cmfgen_norot', temp, metal, grav) - - # Rebin - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - # Make the FITS file from the columns with header - cols = fits.ColDefs([c0,c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - # Write hdu to new directory with same filename - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(path+files_all[ff]) - - print( 'Finished file {0} of {1}'.format(count, len(files_all)) ) - return - - -def organize_PHOENIXv16_atmospheres(path_to_dir, met_str='m00'): - """ - Construct the Phoenix Husser+13 atmopsheres for each model. Combines the - fluxes from the *HiRES.fits files and the wavelengths of the - WAVE_PHONEIX-ACES-AGSS-COND-2011.fits file. - - path_to_dir is the path to the directory containing all of the downloaded - files - - met_str is the name of the current metallicity - - Creates new fits files for each atmosphere: phoenix_.fits, - which contains columns for the log g (column header = g#.#). Puts - atmospheres in new directory phoenixm00 - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the current metallicity subdirectory - sub_dir = '../phoenix{0}'.format(met_str) - if os.path.exists(sub_dir): - pass - else: - os.mkdir(sub_dir) - - # Extract wavelength array, make column for later - wavefile = fits.open('WAVE_PHOENIX-ACES-AGSS-COND-2011.fits') - wave = wavefile[0].data - wavefile.close() - wave_col = Column(wave, name = 'WAVELENGTH') - - # Create temp array for Husser+13 grid (given in paper) - temp_arr = np.arange(2300, 7001, 100) - temp_arr = np.append(temp_arr, np.arange(7000, 12001, 200)) - - print( 'Looping though all temps') - # For each temp, build file containing the flux for all gravities - i = 0 - for temp in temp_arr: - files = glob.glob('lte{0:05d}-*-HiRes.fits'.format(temp)) - files.sort() - # Start the table with the wavelength column - t = Table() - t.add_column(wave_col) - for f in files: - # Extract the logg out of filename - logg = f[9:13] - - # Extract fluxes from file - spectrum = fits.open(f) - flux = spectrum[0].data - spectrum.close() - - # Make Column object with fluxes, add to table - col = Column(flux, name = 'g{0:2.1f}'.format(float(logg))) - t.add_column(col) - - # Now, construct final fits file for the given temp - outname = 'phoenix{0}_{1:05d}.fits'.format(met_str, temp) - t.write('{0}/{1}'.format(sub_dir, outname), format = 'fits', overwrite = True) - - # Progress counter for user - i += 1 - print( 'Done {0:d} of {1:d}'.format(i, len(temp_arr))) - - # Return to original directory - os.chdir(start_dir) - return - -def make_PHOENIXv16_catalog(path_to_dir, met_str='m00'): - """ - Makes catalog.fits file for Husser+13 phoenix models. Assumes that - organize_PHOENIXv16_atmospheres has been run already, and that the models lie - in subdirectory phoenix[met_str]. - - path_to_directory is the path to the directory with the reformatted - models (i.e. the output from construct_atmospheres, phoenix[met_str]) - - Puts catalog.fits file in directory the user starts in - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Extract metallicity from metallicity string - met = float(met_str[1]) + (float(met_str[2]) * 0.1) - if 'm' in met_str: - met *= -1. - - # Collect the filenames. Each is a unique temp with many different log g's - files = glob.glob('phoenix*.fits') - files.sort() - - # Create the catalog.fits file, row by row - index_arr = [] - filename_arr = [] - for i in files: - # Get log g values from the column header the file - t = Table.read(i, format='fits') - keys = t.keys() - logg_vals = keys[1:] - - # Extract temp from filename - name = i.split('_') - temp = float(name[1][:-5]) - for j in logg_vals: - logg = float(j[1:]) - index = '{0:5.0f},{1:2.1f},{2:2.1f}'.format(temp, met, logg) - filename = path_to_dir + i + '[' + j + ']' - # Add row to table - index_arr.append(index) - filename_arr.append(filename) - - catalog = Table([index_arr, filename_arr], names=('INDEX', 'FILENAME')) - - # Return to starting directory, write catalog - os.chdir(start_dir) - - if os.path.exists('catalog.fits'): - from astropy.table import vstack - - prev_catalog = Table.read('catalog.fits', format='fits') - joined_catalog = vstack([prev_catalog, catalog]) - - joined_catalog.write('catalog.fits', format='fits', overwrite=True) - else: - catalog.write('catalog.fits', format='fits', overwrite=True) - - return - -def cdbs_PHOENIXv16(path_to_cdbs_dir): - """ - Put the PHOENIXv16 (Husser+13) fits files into cdbs format. This primarily - consists of adjusting the flux units from [erg/s/cm^2/cm] to [erg/s/cm^2/A] - and adding the appropriate keywords to the fits header. - - path_to_cdbs_dir goes from current working directory to phoenix[met] directory - in cdbs/grids/phoenix_v16. Note that these files have already been organized - using organize_PHOENIXv16_atmospheres code. - - Overwrites original files in directory - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_cdbs_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('phoenix*.fits') - - ## Need to sort filenames; glob doesn't always give them in order - files.sort() - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - - # Read in current FITS table - cur_table = Table.read(i, format='fits') - - cur_table.columns[0].name = 'Wavelength' - - num_cols = len(cur_table.colnames) - - # Multiplying each flux column by 10^-8 for conversion - for cur_col_index in range(1, num_cols, 1): - cur_col_name = cur_table.colnames[cur_col_index] - cur_table[cur_col_name] = cur_table[cur_col_name] * 10.**-8 - - - # Construct new FITS file based on old one - hdu = fits.open(i) - header_0 = hdu[0].header - header_1 = hdu[1].header - sci = hdu[1].data - - tbhdu = fits.table_to_hdu(cur_table) - - # Copying over the older headers, adding unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - tbhdu.header['TUNIT3'] = 'FLAM' - tbhdu.header['TUNIT4'] = 'FLAM' - tbhdu.header['TUNIT5'] = 'FLAM' - tbhdu.header['TUNIT6'] = 'FLAM' - tbhdu.header['TUNIT7'] = 'FLAM' - tbhdu.header['TUNIT8'] = 'FLAM' - tbhdu.header['TUNIT9'] = 'FLAM' - tbhdu.header['TUNIT10'] = 'FLAM' - tbhdu.header['TUNIT11'] = 'FLAM' - tbhdu.header['TUNIT12'] = 'FLAM' - tbhdu.header['TUNIT13'] = 'FLAM' - tbhdu.header['TUNIT14'] = 'FLAM' - - # Construct and write out final FITS file - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i, overwrite=True) - - hdu.close() - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Change back to starting directory - os.chdir(start_dir) - - return - -def rebin_phoenixV16(cdbs_path): - """ - Rebin phoenixV16 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: phoenix_v16_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - ## (This assumes that at least 'm00' metallicity exists) - tmp = '{0}/grid/phoenix_v16/phoenix{1}/phoenix{1}_02400.fits'.format(cdbs_path, 'm00') - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/phoenix_v16_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/phoenix_v16/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - temp_arr = np.zeros(len(files_all), dtype=float) - logg_arr = np.zeros(len(files_all), dtype=float) - metal_arr = np.zeros(len(files_all), dtype=float) - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - - temp_arr[ff] = float(vals[0]) - metal_arr[ff] = float(vals[1]) - logg_arr[ff] = float(vals[2]) - - - metal_uniq = np.unique(metal_arr) - temp_uniq = np.unique(temp_arr) - - for mm in range(len(metal_uniq)): - metal = metal_uniq[mm] # metallicity - - # Construct str for metallicity (for appropriate directory name) - met_str = str(int(np.abs(metal))) + str(int((metal % 1.0)*10)) - if metal > 0: - met_str = 'p' + met_str - else: - met_str = 'm' + met_str - - # Make directory for current metallicity if it does not exist yet - if not os.path.exists(path + 'phoenix' + met_str): - os.mkdir(path + 'phoenix' + met_str) - - for tt in range(len(temp_uniq)): - temp = temp_uniq[tt] # temperature - - # Pick out the list of gravities for this T, Z combo - idx = np.where((metal_arr == metal) & (temp_arr == temp))[0] - logg_exist = logg_arr[idx] - - # All gravities will go in one file. Here is the output - # file name. - outfile = path + files_all[idx[0]].split('[')[0] - - ## If the rebinned file already exists, continue - if os.path.exists(outfile): - continue - - # Build a columns array. One column for each gravity. - cols_arr = [] - - # Make the wavelength column, which is first in the cols array. - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - cols_arr.append(c0) - - for gg in range(len(logg_exist)): - grav = logg_exist[gg] # gravity - - # Fetch the spectrum - sp = pysynphot.Icat('phoenix_v16', temp, metal, grav) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Store the spectrum - name = 'g{0:3.1f}'.format(grav) - col = fits.Column(name=name, format='E', array=flux_rebin) - cols_arr.append(col) - - - # Make the FITS file from the columns with header. - cols = fits.ColDefs(cols_arr) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - for gg in range(len(logg_exist)): - tbhdu.header['TUNIT{0:d}'.format(gg+2)] = 'FLAM' - - # Write hdu - finalhdu = fits.HDUList([prihdu, tbhdu]) - # don't have overwrite to protect original files. - finalhdu.writeto(outfile) - - print( 'Finished file ' + outfile + ' with gravities: ', logg_exist) - - - return - - -def rebin_spec(wave, specin, wavnew): - """ - Helper routine to rebin spectra. TAKEN FROM ASTROBETTER BLOG FROM JESSICA: - http://www.astrobetter.com/blog/2013/08/12/ - python-tip-re-sampling-spectra-with-pysynphot/ - """ - spec = pysynphot.spectrum.ArraySourceSpectrum(wave=wave, flux=specin) - f = np.ones(len(wave)) - filt = pysynphot.spectrum.ArraySpectralElement(wave, f, waveunits='angstrom') - obs = pysynphot.observation.Observation(spec, filt, binset=wavnew, force='taper') - - return obs.binflux - -def organize_BTSettl_2015_atmospheres(path_to_dir): - """ - Construct cdbs-ready BTSettl_CIFITS_2011_2015 atmospheres for each model. - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/grid/BTSettl_2015 - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the BTSettl subdirectory - if not os.path.exists('BTSettl_2015'): - os.mkdir('BTSettl_2015') - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.spec.fits') - - for i in files: - hdu = fits.open(i) - spec = hdu[1].data - header_0 = hdu[0].header - header_1 = hdu[1].header - - wave = spec.field(0) - flux = spec.field(1) - - # Get units right: convert wave from microns to Angstroms, - # flux from W /m^2/ micron to erg/s/cm^2/A - wave_new = wave * 10**4 - flux_new = flux * 10**(-1) - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave_new) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'grid/BTSettl_2015/'+i, overwrite=True) - - hdu.close() - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_BTSettl_2015_catalog(path_to_dir): - """ - Create cdbs catalog.fits of BTSettl_CIFITS2011_2015 grid. - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*spec.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('-') - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl_2015(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin BTSettle_CIFITS2011_2015 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: BTSettl_2015_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - tmp = cdbs_path+'/grid/phoenix_v16/phoenixm00/phoenixm00_02400.fits' - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - phoenix_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/BTSettl_2015_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/BTSettl_2015/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning BTSettl spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - sp = pysynphot.Icat('BTSettl_2015', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - -def make_wavelength_unique(files, dirname): - """ - Helper function to go through each BTSettl spectrum and ensure that - each wavelength point is unique. This is required for rebinning to work. - - - files: list of files to run this analysis on - """ - # Loop through each file, find fix repeated wavelength entries if necessary - for i in files: - t = Table.read('{0}/{1}'.format(dirname,i), format='fits') - test = np.unique(t['Wavelength'], return_index=True) - - if len(t) != len(test[0]): - t = t[test[1]] - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - # Also make sure wavelength is monotonic. If it is not, then it is - # a sign that the wavelengths are out of order - diff = np.diff(t['Wavelength']) - bad = np.where(diff < 0) - if len(bad[0]) > 0: - t.sort('Wavelength') - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - print('Done {0}'.format(i)) - - return - -def organize_BTSettl_atmospheres(): - """ - Construct cdbs-ready atmospheres for the BTSettl grid (CIFITS2011). - The code expects tp be run in cdbs/grid/BTSettl, and expects that the - individual model files have been downloaded from online - (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011/SPECTRA/) - and processed into python-readable ascii files. - """ - orig_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btm10', 'btm05', 'btp00', 'btp05'] - - - # Go through each directory, turning each spectrum into a cdbs-ready file. - # Will convert flux into Ergs/sec/cm**2/A (FLAM) units and save as a fits file, - # for faster access later - for ii in dirs: - print('Starting {0}'.format(ii)) - os.chdir(ii) - - files = glob.glob('*.txt') - count=0 - for jj in files: - t = Table.read(jj, format='ascii') - # First, trim the wavelengths to a more reasonable wavelength range - good = np.where( (t['col1'] > 1000) & (t['col1'] < 70000) ) - t = t[good] - - # Convert flux units to Flam (Ergs/sec/cm**2/A) - flux_new = 10**(t['col2'] - 8.0) - - # Save the file as a fits file - c0 = fits.Column(name='Wavelength', format='D', array=t['col1']) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Add unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto('{0}.fits'.format(jj[:-4]), overwrite=True) - hdu_new.close() - count += 1 - print('Done {0} of {1}'.format(count, len(files))) - - # Now, clean up all the files made when unzipping the spectra - cmd1 = 'rm *.bz2' - cmd2 = 'rm *.tmp' - #cmd3 = 'rm *.txt' - os.system(cmd1) - os.system(cmd2) - #os.system(cmd3) - print('==============================') - print('Done {0}'.format(ii)) - print('==============================') - - # Go back to original directory, move to next metallicity directory - os.chdir(orig_dir) - - return - -def make_BTSettl_catalog(): - """ - Create cdbs catalog.fits of BTSettl grid. - THIS IS STEP 2, after organize_BTSettl_atmospheres has - been run. - - Code expects to be run in cdbs/grid/BTSettl - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btp05'] - - # Construct the catalog.fits file input. The input consists of - # and index string that specifies the stellar paramters, and a - # name string that points to the file - # Loop over all the metallicity directories to construct these inputs - index_str = [] - name_str = [] - for ii in dirs: - os.chdir(ii) - files = glob.glob('*.fits') - - # Construct the metallicity val - if 'm' in ii: - metal_flag = -1 * float(ii[3:])*0.1 - else: - metal_flag = float(ii[3:])*0.1 - - # Now collect the info from the files - for jj in files: - tmp = jj.split('-') - - if metal_flag >= 0: - temp = float(tmp[0].split('+')[0][3:]) * 100.0 # In kelvin - try: - logg = float(tmp[1]) - except: - logg = float(tmp[1].split('+')[0]) - else: - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0},{1},{2:3.2f}'.format(int(temp), metal_flag, logg)) - name_str.append('{0}/{1}[Flux]'.format(ii, jj)) - - # Go back to original directory to move to next metallicity - print('Done {0}'.format(ii)) - os.chdir(start_dir) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl(make_unique=False): - """ - Rebin BTSettle models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory: BTSettl_rebin - - Code expects to be run in cdbs/grid directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Create cdbs/grid directory for rebinned models - path = 'BTSettl_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata('BTSettl/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - #==============================# - #tmp = [] - #for ii in files_all: - # if ii.startswith('btp00'): - # tmp.append(ii) - #files_all = tmp - #=============================# - - print( 'Rebinning BTSettl spectra') - if make_unique: - print('Making unique') - make_wavelength_unique(files_all, 'BTSettl') - print('Done') - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - try: - sp = pysynphot.Icat('BTSettl', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - except: - pdb.set_trace() - orig_file = '{0}/{1}'.format('BTSettl/', files_all[ff].split('[')[0]) - outfile = path + files_all[ff].split('[')[0] - cmd = 'cp {0} {1}'.format(orig_file, outfile) - os.system(cmd) - - print('Done {0} of {1}'.format(ff, len(files_all))) - - return - -def organize_all_Meisner2023_atmospheres(): - """ - Construct cdbs-ready atmospheres for the Meisner2023 grid. - The code expects tp be run in cdbs/grid/Meisner2023, and expects that the - individual model files have been downloaded from online - and processed into python-readable ascii files. - """ - orig_dir = os.getcwd() - dirs = ['mm10', 'mm05', 'mp00', 'mp03'] - - # Go through each directory, turning each spectrum into a cdbs-ready file. - # Save as a fits file, for faster access later - for ii in dirs: - print('Starting {0}'.format(ii)) - os.chdir(ii) - - files = glob.glob('*.fits') - count=0 - for jj in files: - # Open each .fits file and read the data - with fits.open(jj) as hdul: - data = hdul[1].data - wavelength = data['Wavelength'] - flux = data['Flux'] - - # Make flux independent of R&D - flux_new = flux / 5e-20 - - # Create new columns with desired format - c0 = fits.Column(name='Wavelength', format='D', array=wavelength) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Add unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write the new fits table in the cdbs directory - output_filename = '{0}.fits'.format(jj[:-5]) # Removing the original .fits extension - hdu_new.writeto(output_filename, overwrite=True) - hdu_new.close() - count += 1 - print('Done {0} of {1}'.format(count, len(files))) - - # Go back to original directory, move to next metallicity directory - os.chdir(orig_dir) - - return - -def make_Meisner2023_catalog(): - """ - Create cdbs catalog.fits of Meisner2023 grid. - THIS IS STEP 2, after organize_Meisner2023_atmospheres has - been run. - - Code expects to be run in cdbs/grid/Meisner2023 - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - dirs = ['mm10', 'mm05', 'mp00', 'mp03'] - - # Construct the catalog.fits file input. The input consists of - # and index string that specifies the stellar paramters, and a - # name string that points to the file - # Loop over all the metallicity directories to construct these inputs - index_str = [] - name_str = [] - for ii in dirs: - os.chdir(ii) - files = glob.glob('spec_jwst_*.fits') - - for jj in files: - # Parse temperature, log(g), and metallicity from filename - temp_str = jj.split('_')[2] - logg_str = jj.split('_')[3] - metal_str = jj.split('_')[4] - - # Extract temperature, surface gravity, and metallicity - temp = float(temp_str[1:]) # Temperature in Kelvin - logg = float(logg_str[1:]) # Surface gravity log(g) - - # Build metallicity value - if metal_str.startswith('m'): - metallicity = -1 * float(metal_str[1:]) - else: - metallicity = float(metal_str[1:]) - - # Construct index and filename strings - index_str.append('{0},{1},{2:3.2f}'.format(int(temp), metallicity, logg)) - name_str.append('{0}/{1}[Flux]'.format(ii, jj)) - - print('Processed directory:', ii) - os.chdir(start_dir) - - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_Meisner2023(make_unique=False): - """ - Rebin Meisner2023 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory: Meisner2023_rebin - - Code expects to be run in cdbs/grid directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Create a directory for rebinned Meisner2023 models - rebin_path = 'Meisner2023_rebin/' - if not os.path.exists(rebin_path): - os.mkdir(rebin_path) - - # Load the catalog.fits file and extract all spectra file paths - cat = Table.read('Meisner2023/catalog.fits') - files_all = [cat[ii]['FILENAME'].split('[')[0] for ii in range(len(cat))] - - print('Rebinning Meisner2023 spectra') - if make_unique: - print('Making unique') - make_wavelength_unique(files_all, 'Meisner2023') - print('Done') - - for ff, file in enumerate(files_all): - vals = cat[ff]['INDEX'].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the Meisner2023 spectrum and rebin its flux - try: - sp = pysynphot.Icat('Meisner2023', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Create the output FITS file - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - # Write the new rebinned file in the Meisner2023_rebin directory - outfile = os.path.join(rebin_path, os.path.basename(file)) - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - except Exception as e: - print(f"Error processing {file}: {e}") - orig_file = os.path.join('Meisner2023', file) - outfile = os.path.join(rebin_path, os.path.basename(file)) - os.system(f'cp {orig_file} {outfile}') - - print('Done {0} of {1}'.format(ff + 1, len(files_all))) - - return - - - -def organize_WDKoester_atmospheres(path_to_dir): - """ - Construct cdbs-ready wdKoester WD atmospheres for each model. (from Koester 2010) - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/wdKoeseter - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.dk.dat.txt') - - for i in files: - data = Table.read(i, format='ascii') - - wave = data['col1'] # angstrom - flux = data['col2'] # erg/s/cm^2/A - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'/grid/wdKoester/'+i.replace('.txt', '.fits'), overwrite=True) - - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_WDKoester_catalog(path_to_dir): - """ - Create cdbs catalog.fits of wdKoester grid. - THIS IS STEP 2, after organize_WDKoester_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*dk.dat.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('.') - tmp2 = tmp[0].split('_') - temp = float(tmp2[0][2:]) # Kelvin - logg = float(tmp2[1]) / 100.0 # log(g) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_WDKoester(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin wdKoester models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: wdKoester_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing model; we will steal the header - tmp = cdbs_path+'/grid/wdKoester/da70000_800.dk.dat.fits' - wdkoester_hdu = fits.open(tmp) - header0 = wdkoester_hdu[0].header - wdkoester_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/wdKoester_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/wdKoester/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning wdKoester spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the wdKoester spectrum, rebin flux - sp = pysynphot.Icat('wdKoester', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - - diff --git a/spisea/atmospheres_BACKUP_58456.py b/spisea/atmospheres_BACKUP_58456.py deleted file mode 100755 index e3d2233c..00000000 --- a/spisea/atmospheres_BACKUP_58456.py +++ /dev/null @@ -1,2524 +0,0 @@ -import logging -import numpy as np -import pysynphot -import os -import glob -from astropy.io import fits -from astropy.table import Table, Column -import pysynphot -import time -import pdb -import warnings - -log = logging.getLogger('atmospheres') - -def get_atmosphere_bounds(model_dir, metallicity=0, temperature=20000, gravity=4, verbose=False): - """ - Given atmosphere model, get temperature and gravity bounds - """ - # Open catalog fits file and break out row indices - catalog = Table.read('{0}/grid/{1}/catalog.fits'.format(os.environ['PYSYN_CDBS'], model_dir)) - - teff_arr = [] - z_arr = [] - logg_arr = [] - for cur_row_index in range(len(catalog)): - index = catalog['INDEX'][cur_row_index] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Filter by metallicity. Will chose the closest metallicity to desired input - metal_list = np.unique(np.array(z_arr)) - metal_idx = np.argmin(np.abs(metal_list - metallicity)) - metallicity_new = metal_list[metal_idx] - - z_filt = np.where(z_arr == metal_list[metal_idx]) - teff_arr = teff_arr[z_filt] - logg_arr = logg_arr[z_filt] - - # # Now find the closest atmosphere in parameter space to - # # the one we want. We'll find the match with the lowest - # # fractional difference - # teff_diff = (teff_arr - temperature) / temperature - # logg_diff = (logg_arr - gravity) / gravity - # - # diff_tot = abs(teff_diff) + abs(logg_diff) - # idx_f = np.argmin(diff_tot) - # - # temperature_new = teff_arr[idx_f] - # gravity_new = logg_arr[idx_f] - - # First check if temperature within bounds - temperature_new = temperature - if temperature > np.max(teff_arr): - temperature_new = np.max(teff_arr) - if temperature < np.min(teff_arr): - temperature_new = np.min(teff_arr) - - # If temperature within bounds, then check if metallicity within bounds - teff_diff = np.abs(teff_arr - temperature) - sorted_min_diffs = np.unique(teff_diff) - - ## Find two closest temperatures - teff_close_1 = teff_arr[np.where(teff_diff == sorted_min_diffs[0])[0][0]] - teff_close_2 = teff_arr[np.where(teff_diff == sorted_min_diffs[1])[0][0]] - - logg_arr_1 = logg_arr[np.where(teff_arr == teff_close_1)] - logg_arr_2 = logg_arr[np.where(teff_arr == teff_close_2)] - - ## Switch to most conservative bound of logg out of two closest temps - gravity_new = gravity - if gravity > np.min([np.max(logg_arr_1), np.max(logg_arr_2)]): - gravity_new = np.min([np.max(logg_arr_1), np.max(logg_arr_2)]) - if gravity < np.max([np.min(logg_arr_1), np.min(logg_arr_2)]): - gravity_new = np.max([np.min(logg_arr_1), np.min(logg_arr_2)]) - - if verbose: - # Print out changes, if any - if temperature_new != temperature: - teff_msg = 'Changing to T={0:6.0f} for met={1:4.2f} T={2:6.0f} logg={3:4.2f}' - print( teff_msg.format(temperature_new, metallicity, temperature, gravity)) - - if gravity_new != gravity: - logg_msg = 'Changing to logg={0:4.2f} for met={1:4.2f} T={2:6.0f} logg={3:4.2f}' - print( logg_msg.format(gravity_new, metallicity, temperature, gravity)) - - if metallicity_new != metallicity: - logg_msg = 'Changing to met={0:4.2f} for met={1:4.2f} T={2:6.0f} logg={3:4.2f}' - print( logg_msg.format(metallicity_new, metallicity, temperature, gravity)) - - return (temperature_new, gravity_new, metallicity_new) - -def get_kurucz_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmosphere from the Kurucz pysnphot grid - (`Kurucz 1993 `_). - - Grid Range: - - * Teff: 3000 - 50000 K - * gravity: 0 - 5 cgs - * metallicity: -5.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - Always false for this particular function - """ - try: - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('k93models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Kurucz 1993 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_castelli_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmospheres from the pysynphot ATLAS9 atlas - (`Castelli & Kurucz 2004 `_). - - Grid Range: - - * Teff: 3500 - 50000 K - * gravity: 0 - 5.0 cgs - * [M/H]: -2.5 - 0.2 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('ck04models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Castelli and Kurucz 2004 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_nextgen_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - try: - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('nextgen', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find NextGen atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_amesdusty_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - sp = pysynphot.Icat('AMESdusty', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find AMESdusty Allard+ 2000 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenix_atmosphere(metallicity=0, temperature=5000, gravity=4, - rebin=False): - """ - Return atmosphere from the pysynphot - `PHOENIX atlas `_. - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - try: - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIX BT-Settl (Allard+ 2011 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere(metallicity=0, temperature=24000, gravity=4.3, rebin=True, verbose=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - # Take care of atmospheres outside the catalog boundaries - logg_msg = 'Changing to logg={0:3.1f} for T={1:6.0f} logg={2:4.2f}' - if gravity > 4.3: - if verbose: - print( logg_msg.format(4.3, temperature, gravity)) - gravity = 4.3 - - if rebin: - sp = pysynphot.Icat('cmfgen_rot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_rot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere_closest(metallicity=0, temperature=24000, gravity=4.3, rebin=True, - verbose=False): - """ - For a given stellar atmosphere, get extract the closest possible match in - Teff/logg space. Note that this is different from the normal routine - which interpolates along the input grid to get final spectrum. We can't - do this here because the Fierro+15 atmosphere grid is so sparse - - rebin=True: pull from atmospheres at ck04model resolution. - - If verbose, print out the parameters of the match - """ - # Set up the proper root directory - if rebin == True: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot_rebin/' - else: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot/' - - # Read in catalog, extract atmosphere info - cat = Table.read('{0}/catalog.fits'.format(root_dir), format='fits') - teff_arr = [] - z_arr = [] - logg_arr = [] - for ii in range(len(cat)): - index = cat['INDEX'][ii] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Now find the closest atmosphere in parameter space to - # the one we want. We'll find the match with the lowest - # fractional difference - teff_diff = (teff_arr - temperature) / temperature - logg_diff = (logg_arr - gravity) / gravity - - diff_tot = abs(teff_diff) + abs(logg_diff) - idx_f = np.where(diff_tot == min(diff_tot))[0][0] - - # Extract the filename of the best-match model and read as - # pysynphot object - infile = cat[idx_f]['FILENAME'].split('.') - spec = Table.read('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Now, the CMFGEN atmospheres assume a distance of 1 kpc, while the the - # ATLAS models are in FLAM at the surface. So, we need to multiply the - # CMFGEN atmospheres by (1000/R)**2. in order to convert to FLAM on surface. - # We'll calculate radius from Teff and logL, which is given in the Table_*.txt file - t = Table.read('{0}/Table_rot.txt'.format(root_dir), format='ascii') - tmp = np.where(t['col1'] == infile[0]) - - lum = t['col3'][tmp] * (3.839*10**33) # cgs - sigma = 5.6704 * 10**-5 # cgs - teff = teff_arr[idx_f] # cgs - - radius = np.sqrt( lum / (4.0 * np.pi * teff**4. * sigma) ) # in cm - radius /= 3.08*10**18 # in pc - - - # Make the pysynphot spectrum - w = spec['Wavelength'] - f = spec['Flux'] * (1000 / radius)**2. - sp = pysynphot.ArraySpectrum(w,f) - - #sp = pysynphot.FileSpectrum('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Print out parameters of match, if desired - if verbose: - print('Teff match: Input: {0}, Output: {1}'.format(temperature, teff_arr[idx_f])) - print('logg match: Input: {0}, Output: {1}'.format(gravity, logg_arr[idx_f])) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=22500, gravity=3.98, rebin=True): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - if rebin: - sp = pysynphot.Icat('cmfgen_norot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_norot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=30000, gravity=4.14): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 30000) - gravity = log gravity (def = 4.14) - """ - sp = pysynphot.Icat('cmfgenF15_noRot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN non-rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenixv16_atmosphere(metallicity=0, temperature=4000, gravity=4, rebin=True): - """ - Return PHOENIX v16 atmospheres from - `Husser et al. 2013 `_. - - Models originally downloaded via `ftp `_. - Solar metallicity and [alpha/Fe] is used. - - Grid Range: - - * Teff: 2300 - 7000 K, steps of 100 K; 7000 - 12000 in steps of 200 K - * gravity: 0.0 - 6.0 cgs, steps of 0.5 - * [M/H]: -4.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - atm_model_name = 'phoenix_v16' - if rebin == True: - atm_model_name = 'phoenix_v16_rebin' - - - # Extract atmosphere. If that fails, then check bounds and try again - try: - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds(atm_model_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIXv16 (Husser+13) atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_2015_atmosphere(metallicity=0, temperature=2500, gravity=4, rebin=True): - """ - Return atmosphere from CIFIST2011_2015 grid - (`Allard et al. 2012 `_, - `Baraffe et al. 2015 `_ ) - - Grid originally downloaded from `website `_. - - Grid Range: - - * Teff: 1200 - 7000 K - * gravity: 2.5 - 5.5 cgs - * [M/H] = 0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - if rebin == True: - atm_name = 'BTSettl_2015_rebin' - else: - atm_name = 'BTSettl_2015' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) -<<<<<<< HEAD - #print(dir(obj)) - - -======= - - ->>>>>>> upstream/dev - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl_2015 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_atmosphere(metallicity=0, temperature=2500, gravity=4.5, rebin=True): - """ - Return atmosphere from CIFIST2011 grid - (`Allard et al. 2012 `_) - - Grid originally downloaded `here `_ - - Notes - ------ - Grid Range: - - * [M/H] = -2.5, -2.0, -1.5, -1.0, -0.5, 0, 0.5 - - Teff and gravity ranges depend on metallicity: - - [M/H] = -2.5 - - * Teff: 2600 - 4600 K - * gravity: 4.5 - 5.5 - - [M/H] = -2.0 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.5 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.0 - - * Teff: 2600 - 7000 - * gravity: Teff < 3200 --> 4.5 - 5.5; Teff > 3200 --> 2.5 - 5.5 - - [M/H] = -0.5 - - * Teff: 1000 -7000 - * gravity: Teff < 3000 --> 4.5 - 5.5; Teff > 3000 --> 3.0 - 6.0 - - [M/H] = 0 - - * Teff: 750 - 7000 - * gravity: Teff < 2500 --> 3.5 - 5.5; Teff > 2500 --> 0 - 5.5 - - [M/H] = 0.5 - - * Teff: 1000 - 5000 - * gravity: 3.5 - 5.0 - - - Alpha enhancement: - - * [M/H]= -0.0, +0.5 no anhancement - * [M/H]= -0.5 with [alpha/H]=+0.2 - * [M/H]= -1.0, -1.5, -2.0, -2.5 with [alpha/H]=+0.4 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - **PRINT STATEMENTS TO DEBUG - **check get_atmosphere_bounds - **comment out try/except clause and check break - """ - if rebin == True: - atm_name = 'BTSettl_rebin' - else: - atm_name = 'BTSettl' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) -<<<<<<< HEAD - -def get_Meisner2023_atmosphere(metallicity=0, temperature=1000, gravity=4.5, rebin=True): - """ - Return atmosphere from Meisner2023 grid - (`Meisner et al. 2023 `_) - - Grid originally downloaded `here `_ - - Grid range: - * Teff = 250 - 1200 K - * gravity: 2.5 - 5.5 cgs (in steps of 0.5) - * [M/H] = -1.0, -0.5, 0, +0, +0.3 - - """ - if rebin == True: - atm_name = 'Meisner2023_rebin' - else: - atm_name = 'Meisner2023' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) -======= - ->>>>>>> upstream/dev - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Meisner2023 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_Phillips2020_atmosphere(metallicity=0, temperature=1000, gravity=4.5, rebin=True): - """ - Return atmosphere from Phillips et al., 2020 using ATMO model - (`Phillips et al. 2020 `_) - - Grid originally downloaded `here `_ - - Grid Range: - * Teff: 200 - 3000 K - * gravity: 2.5 - 5.5 cgs - * [M/H] = 0 - """ - if rebin == True: - atm_name = 'Phillips2020_rebin' - else: - atm_name = 'Phillips2020' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Phillips2020 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_wdKoester_atmosphere(metallicity=0, temperature=20000, gravity=7): - """ - Return white dwarf atmospheres from - `Koester et al. 2010 `_ - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - sp = pysynphot.Icat('wdKoester', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find WD Koester (Koester+ 2010 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_atlas_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of atlas ck04 model and phoenixV16. - - Only valid for temps between 5000 - 5500K, gravity from 0 = 5.0 - """ - try: - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('merged_atlas_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model - and phoenixV16. - - Only valid for temps between 3200 - 3800K, gravity from 2.5 - 5.5 - """ - try: - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('merged_BTSettl_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_meisner_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model - and Meisner2023. - - Only valid for temps between 1000 - 1200K, gravity from 3.5 - 5.5 - """ - try: - sp = pysynphot.Icat('merged_BTSettl_meisner', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('merged_BTSettl_meisner', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_BTSettl_meisner', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl-Meisner merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -#---------------------------------------------------------------------# -def get_merged_atmosphere(metallicity=0, temperature=20000, gravity=4.5, verbose=False, - rebin=True): - """ - Return a stellar atmosphere from a suite of different model grids, - depending on the input temperature, (all values in K). - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - - Notes - ----- - The underlying stellar model grid used changes as a function of - stellar temperature (in K): - - * T > 20,000: ATLAS - * 5500 <= T < 20,000: ATLAS - * 5000 <= T < 5500: ATLAS/PHOENIXv16 merge - * 3800 <= T < 5000: PHOENIXv16 - - For T < 3800, there is an additional gravity and metallicity - dependence: - - If T < 3800 and [M/H] = 0: - - * T < 3800, logg < 2.5: PHOENIX v16 - * 3200 <= T < 3800, logg > 2.5: BTSettl_CIFITS2011_2015/PHOENIXV16 merge - * 3200 < T <= 1200, logg > 2.5: BTSettl_CIFITS2011_2015 - * 1200 < T <= 1000, logg >= 3.5: BTSettl_CIFITS2011_2015/Meisner2023 merge - * 1000 < T <= 250, logg > 2.5: Meisner2023 - - Otherwise, if T < 3800 and [M/H] != 0: - - * T < 3800: PHOENIX v16 - - References: - - * ATLAS: ATLAS9 models (`Castelli & Kurucz 2004 `_) - * PHOENIXv16 (`Husser et al. 2013 `_) - * BTSettl_CIFITS2011_2015: Baraffee+15, Allard+ (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011_2015/SPECTRA/) - * Meisner2023: ATMO 1D models (`Meisner et al. 2023 `_) - - LTE WARNING: - - The ATLAS atmospheres are calculated with LTE, and so they - are less accurate when non-LTE conditions apply (e.g. T > 20,000 - K). Ultimately we'd like to add a non-LTE atmosphere grid for - the hottest stars in the future. - - HOW BOUNDARIES BETWEEN MODELS ARE TREATED: - - At the boundary between two models grids a temperature range is defined - where the resulting atmosphere is a weighted average between the two - grids. Near one boundary one model - is weighted more heavily, while at the other boundary the other - model is weighted more heavily. These are calculated in the - temperature ranges where we switch between model grids, to - ensure a smooth transition. - """ - # For T < 3800, atmosphere depends on metallicity + gravity. - # If solar metallicity, use BTSettl 2015 grid. Only solar metallicity is - # currently available here, so if non-solar metallicity, just stick with - # the Phoenix grid - if (temperature < 1000): - if verbose: - print( 'Meisner2023 atmosphere') - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 1200) & (temperature >= 1000): - if (gravity >= 3.5): - if verbose: - print( 'BTSettl/Meisner2023 merged atmosphere') - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - if (gravity < 3.5) & (gravity >=2.5): - if verbose: - print( 'Meisner2023 atmosphere') - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 3800) & (metallicity == 0): - # High gravity are in BTSettl regime - if (temperature <= 3200) & (gravity > 2.5): - if verbose: - print( 'BTSettl_2015 atmosphere') - return get_BTSettl_2015_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 3200) & (temperature < 3800) & (gravity > 2.5): - if verbose: - print( 'BTSettl/Phoenixv16 merged atmosphere') - return get_BTSettl_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - # Low gravity is PHOENIX regime - if gravity <= 2.5: - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 3800) & (metallicity != 0): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - # For T > 3800, no metallicity or gravity dependence - if (temperature >= 3800) & (temperature < 5000): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 5000) & (temperature < 5500): - if verbose: - print( 'ATLAS/Phoenix merged atmosphere') - return get_atlas_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if (temperature >= 5500) & (temperature < 20000): - if verbose: - print( 'ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if temperature >= 20000: - if verbose: - print( 'Still ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - #print('CMFGEN') - #return get_cmfgenRot_atmosphere_closest(metallicity=metallicity, - # temperature=temperature, - # gravity=gravity) - - - - -def get_wd_atmosphere(metallicity=0, temperature=20000, gravity=4, verbose=False): - """ - Return the white dwarf atmosphere from - `Koester et al. 2010 `_. - If desired parameters are - outside of grid, return a blackbody spectrum instead - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - if verbose: - print('wdKoester atmosphere') - - return get_wdKoester_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - except pysynphot.exceptions.ParameterOutOfBounds: - # Use a black-body atmosphere. - bbspec = get_bb_atmosphere(temperature=temperature, verbose=verbose) - return bbspec - - -def get_bd_atmosphere(metallicity=0, temperature=1000, gravity=4, verbose=False): - """ - Return the brown dwarf atmosphere from - `Meisner et al. 2023 `_. - If desired parameters are - outside of grid, return a blackbody spectrum instead - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - if verbose: - print('Meisner2023 atmosphere') - - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - except pysynphot.exceptions.ParameterOutOfBounds: - # Use a black-body atmosphere - bbspec = get_bb_atmosphere(temperature=temperature, verbose=verbose) - return bbspec - -def get_bb_atmosphere(metallicity=None, temperature=20_000, gravity=None, - verbose=False, rebin=None, - wave_min=500, wave_max=50_000, wave_num=20_000): - """ - Return a blackbody spectrum - - Parameters - ---------- - temperature: float, default=20_000 - The stellar temperature, in units of K - wave_min: float, default=500 - Sets the minimum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_max: float, default=50_000 - Sets the maximum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_num: int, default=20_000 - Sets the number of wavelength points in the wavelength range - Note: the wavelength range is evenly spaced in log space - """ - if ((metallicity is not None) or (gravity is not None) or - (rebin is not None)): - warnings.warn( - 'Only `temperature` keyword is used for black-body atmosphere' - ) - - if verbose: - print('Black-body atmosphere') - - # Modify pysynphot's default waveset to specified bounds - pysynphot.refs.set_default_waveset( - minwave=wave_min, maxwave=wave_max, num=wave_num - ) - - # Get black-body atmosphere for specified temperature from pysynphot - bbspec = pysynphot.spectrum.BlackBody(temperature) - - # pysynphot `BlackBody` generates spectrum in `photlam`, need in `flam` - bbspec.convert('flam') - - # `BlackBody` spectrum is normalized to solar radius star at 1 kiloparsec. - # Need to remove this normalization for SPISEA by multiplying bbspec - # by (1000 * 1 parsec / 1 Rsun)**2 = (1000 * 3.08e18 cm / 6.957e10 cm)**2 - bbspec *= (1000 * 3.086e18 / 6.957e10)**2 - - return bbspec - - -#--------------------------------------# -# Atmosphere formatting functions -#--------------------------------------# - -def download_CMFGEN_atmospheres(Table_rot, Table_norot): - """ - Downloads CMFGEN models from - https://sites.google.com/site/fluxesandcontinuum/home; - these contain continuum as well as lines. - - Table_rot, Table_norot are tables with the file prefixes - and model atmosphere parameters, taken by hand from the - Fierro+15 paper - - Website addresses are hardcoded - - Puts downloaded models in the current working directory. - """ - print( 'WARNING: THIS DOES NOT COMPLETELY WORK') - print( '**********************') - t_rot = Table.read(Table_rot, format='ascii') - t_norot = Table.read(Table_norot, format='ascii') - - tables = [t_rot, t_norot] - filenames = [t_rot['col1'], t_norot['col1']] - - # Hardcoded list of webiste addresses - web_base1 = 'https://sites.google.com/site/fluxesandcontinuum/home/' - web_base2 = 'https://sites.google.com/site/modelsobmassivestars/' - web = [web_base1+'009-solar-masses/',web_base1+'012-solar-masses/', - web_base1+'015-solar-masses/',web_base1+'020-solar-masses/', - web_base1+'025-solar-masses/',web_base2+'009-solar-masses-tracks/', - web_base2+'040-solar-masses/',web_base2+'060-solar-masses/', - web_base1+'085-solar-masses/',web_base1+'120-solar-masses/'] - # Array of masses that matches the website addresses - mass_arr = np.array([9.,12.,15.,20.,25.,32.,40.,60.,85.,120.]) - - # Loop through rotating and unrotating case. First loop is rot, second unrot - for i in range(2): - # Extract masses from filenames - masses = [] - for j in filenames[i]: - tmp = j.split('m') - mass = float(tmp[1][:-1]) - masses.append(mass) - - # Download the models webpage by webpage. A bit tricky because masses - # change slightly within a particular website. THIS IS WHAT FAILS - for j in range(len(web)): - if j == 0: - good = np.where( (masses <= mass_arr[j]) ) - else: - g = j - 1 - good = np.where( (masses <= mass_arr[j]) & - (masses > mass_arr[g]) ) - # Use wget command to pull down the files, and unzip them - for k in good[0]: - full = web[j]+'{1:s}.flx.zip'.format(mass_arr[j],filenames[i][k]) - os.system('wget ' + full) - os.system('unzip '+ filenames[i][k] + '.flx.zip') - - return - -def organize_CMFGEN_atmospheres(path_to_dir): - """ - Organize CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html) - into rot and noRot directories - - path_to_dir is from current working directory to directory - containing the downloaded models. Assumed that models - and tables describing parameters are in this directory. - - Tables describing parameters MUST be named Table_rot.txt, - Table_noRot.txt. Made by hand from Tables 3, 4 in Fierro+15. - These are located in same original directory as atmosphere files - - Will separate files into 2 subdirectories, one rotating and - the other non-rotating - - *Can't have any other files starting with "t" in model directory to start!* - """ - # First, record current working directory to return to later - start_dir = os.getcwd() - - # Enter atmosphere directory, collect rotating and non-rotating - # file names (assumed to all start with "t") - os.chdir(path_to_dir) - rot_models = glob.glob("t*r.flx*") - noRot_models = glob.glob("t*n.flx*") - - # Separate into different subdirectories - if os.path.exists('cmfgenF15_rot'): - pass - else: - os.mkdir('cmfgenF15_rot') - os.mkdir('cmfgenF15_noRot') - - for mod in rot_models: - cmd = 'mv {0:s} cmfgenF15_rot'.format(mod) - os.system(cmd) - - for mod in noRot_models: - cmd = 'mv {0:s} cmfgenF15_noRot'.format(mod) - os.system(cmd) - - # Also move Tables with model parameters into correct directory - os.system('mv Table_rot.txt cmfgenF15_rot') - os.system('mv Table_noRot.txt cmfgenF15_noRot') - - # Return to original directory - os.chdir(start_dir) - - return - -def make_CMFGEN_catalog(path_to_dir): - """ - Create cdbs catalog.fits of CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html). - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to directory - containing the rotating or non-rotating models (i.e. cmfgenF15_rot). Also, - needs to be a Table*.txt file which contains the parameters for all of the - original models, since params in filename are not precise enough - - Will create catalog.fits file in atmosphere directory with - description of each model - - *Can't have any other files starting with "t" in model directory to start!* - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere - # Note: can't rely on filename for this because not precise enough!! - - #---------OLD: GETTING PARAMS FROM FILENAME-------# - # Collect file names (assumed to all start with "t") - #files = glob.glob("t*") - #for name in files: - # tmp = name.split('l') - # temp = float(tmp[0][1:]) * 100.0 # In kelvin - - # lumtmp = tmp[1].split('_') - # lum = float(lumtmp[0][:-5]) * 1000.0 # In L_sun - - # mass = float(lumtmp[0][5:-1]) # In M_sun - - # Need to calculate log g from T and L (cgs) - # lum_sun = 3.846 * 10**33 # erg/s - # M_sun = 2 * 10**33 # g - # G_si = 6.67 * 10**(-8) # cgs - # sigma_si = 5.67 * 10**(-5) # cgs - - # g = (G_si * mass * M_sun * 4 * np.pi * sigma_si * temp**4) / \ - # (lum * lum_sun) - # logg = np.log10(g) - #---------------------------------------------------# - - # Read table with atmosphere params - table = glob.glob('Table_*') - t = Table.read(table[0], format = 'ascii') - names = t['col1'] - temps = t['col2'] - logg = t['col4'] - - # Create catalog.fits file - index_str = [] - name_str = [] - for i in range(len(names)): - index = '{0:5.0f},0.0,{1:3.2f}'.format(temps[i], logg[i]) - - #---NOTE: THE FOLLOWING DEPENDS ON FINAL LOCATION OF CATALOG FILE---# - #path = path_to_dir + '/' + names[i] - path = names[i] + '.fits[Flux]' - - index_str.append(index) - name_str.append(path) - - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits') - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def cdbs_cmfgen(path_to_dir, path_to_cdbs_dir): - """ - Code to put cmfgen models into cdbs format and adds proper unit keyword in - fits header. Save as fits file - - path_to_dir goes from current directory to cmfgen_rot or cmfgen_norot - directory with the *.flx models. Note that these files have already been - organized using organize_CMFGEN_atmospheres code. - - path_to_cdbs_dir goes from current directory to cdbs/grid/cmfgen_rot or - cmfgen_norot directory. Will copy new fits files to this directory. - This directory must already exist! - """ - # Save starting directory for later, move into path_to_dir directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('*.flx') - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - # Open file, extract useful info - t = Table.read(i, format='ascii') - wave = t['col1'] - flux = t['col2'] # Flux is already in erg/cm^2/s/A - - # Need to eliminate duplicate entries (pysynphot crashes) - unique = np.unique(wave, return_index=True) - wave = wave[unique[1]] - flux = flux[unique[1]] - - # Make fits table from individual columns. - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - #Adding unit keywords - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - prihdu = fits.PrimaryHDU() - - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i[:-4]+'.fits', overwrite=True) - - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Return to original directory, copy over new .fits files to cdbs directory - os.chdir(start_dir) - cmd = 'mv {0:s}/*.fits {1:s}'.format(path_to_dir, path_to_cdbs_dir) - os.system(cmd) - - return - -def rebin_cmfgen(cdbs_path, rot=True): - """ - Rebin cmfgen_rot and cmfgen_norot models to atlas ck04 resolution; - this makes spectrophotometry MUCH faster - - cdbs_path: path to cdbs directory - rot=True for rotating models (cmfgen_rot), False for non-rotating models - - makes new directory in cdbs/grid: cmfgen_rot_rebin or cmfgen_norot_rebin - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing cmfgen model; we will steal the header. - # Also define paths to new rebin directories - if rot == True: - tmp = cdbs_path+'/grid/cmfgen_rot/t0200l0008m009r.fits' - path = cdbs_path+'/grid/cmfgen_rot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_rot/' - else: - tmp = cdbs_path+'/grid/cmfgen_norot/t0200l0007m009n.fits' - path = cdbs_path+'/grid/cmfgen_norot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_norot/' - - cmfgen_hdu = fits.open(tmp) - header0 = cmfgen_hdu[0].header - # Create rebin directories if they don't already exist. Copy over - # catalog.fits file from original directory (will be the same) - if not os.path.exists(path): - os.mkdir(path) - cmd = 'cp {0:s}catalog.fits {1:s}'.format(orig_path, path) - os.system(cmd) - - # Read in the catalog.fits file - cat = fits.getdata(orig_path + 'catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - # First column in new files will be for [atlas] wavelength - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - - # For each catalog.fits entry, read the unbinned spectrum and rebin to - # the atlas resolution. Make a new fits file in rebin directory - count = 0 - for ff in range(len(files_all)): - count += 1 - # Extract the temp, Z, logg - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - grav = float(vals[2]) - - # Fetch the spectrum - if rot == True: - sp = pysynphot.Icat('cmfgen_rot', temp, metal, grav) - else: - sp = pysynphot.Icat('cmfgen_norot', temp, metal, grav) - - # Rebin - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - # Make the FITS file from the columns with header - cols = fits.ColDefs([c0,c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - # Write hdu to new directory with same filename - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(path+files_all[ff]) - - print( 'Finished file {0} of {1}'.format(count, len(files_all)) ) - return - - -def organize_PHOENIXv16_atmospheres(path_to_dir, met_str='m00'): - """ - Construct the Phoenix Husser+13 atmopsheres for each model. Combines the - fluxes from the *HiRES.fits files and the wavelengths of the - WAVE_PHONEIX-ACES-AGSS-COND-2011.fits file. - - path_to_dir is the path to the directory containing all of the downloaded - files - - met_str is the name of the current metallicity - - Creates new fits files for each atmosphere: phoenix_.fits, - which contains columns for the log g (column header = g#.#). Puts - atmospheres in new directory phoenixm00 - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the current metallicity subdirectory - sub_dir = '../phoenix{0}'.format(met_str) - if os.path.exists(sub_dir): - pass - else: - os.mkdir(sub_dir) - - # Extract wavelength array, make column for later - wavefile = fits.open('WAVE_PHOENIX-ACES-AGSS-COND-2011.fits') - wave = wavefile[0].data - wavefile.close() - wave_col = Column(wave, name = 'WAVELENGTH') - - # Create temp array for Husser+13 grid (given in paper) - temp_arr = np.arange(2300, 7001, 100) - temp_arr = np.append(temp_arr, np.arange(7000, 12001, 200)) - - print( 'Looping though all temps') - # For each temp, build file containing the flux for all gravities - i = 0 - for temp in temp_arr: - files = glob.glob('lte{0:05d}-*-HiRes.fits'.format(temp)) - files.sort() - # Start the table with the wavelength column - t = Table() - t.add_column(wave_col) - for f in files: - # Extract the logg out of filename - logg = f[9:13] - - # Extract fluxes from file - spectrum = fits.open(f) - flux = spectrum[0].data - spectrum.close() - - # Make Column object with fluxes, add to table - col = Column(flux, name = 'g{0:2.1f}'.format(float(logg))) - t.add_column(col) - - # Now, construct final fits file for the given temp - outname = 'phoenix{0}_{1:05d}.fits'.format(met_str, temp) - t.write('{0}/{1}'.format(sub_dir, outname), format = 'fits', overwrite = True) - - # Progress counter for user - i += 1 - print( 'Done {0:d} of {1:d}'.format(i, len(temp_arr))) - - # Return to original directory - os.chdir(start_dir) - return - -def make_PHOENIXv16_catalog(path_to_dir, met_str='m00'): - """ - Makes catalog.fits file for Husser+13 phoenix models. Assumes that - organize_PHOENIXv16_atmospheres has been run already, and that the models lie - in subdirectory phoenix[met_str]. - - path_to_directory is the path to the directory with the reformatted - models (i.e. the output from construct_atmospheres, phoenix[met_str]) - - Puts catalog.fits file in directory the user starts in - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Extract metallicity from metallicity string - met = float(met_str[1]) + (float(met_str[2]) * 0.1) - if 'm' in met_str: - met *= -1. - - # Collect the filenames. Each is a unique temp with many different log g's - files = glob.glob('phoenix*.fits') - files.sort() - - # Create the catalog.fits file, row by row - index_arr = [] - filename_arr = [] - for i in files: - # Get log g values from the column header the file - t = Table.read(i, format='fits') - keys = t.keys() - logg_vals = keys[1:] - - # Extract temp from filename - name = i.split('_') - temp = float(name[1][:-5]) - for j in logg_vals: - logg = float(j[1:]) - index = '{0:5.0f},{1:2.1f},{2:2.1f}'.format(temp, met, logg) - filename = path_to_dir + i + '[' + j + ']' - # Add row to table - index_arr.append(index) - filename_arr.append(filename) - - catalog = Table([index_arr, filename_arr], names=('INDEX', 'FILENAME')) - - # Return to starting directory, write catalog - os.chdir(start_dir) - - if os.path.exists('catalog.fits'): - from astropy.table import vstack - - prev_catalog = Table.read('catalog.fits', format='fits') - joined_catalog = vstack([prev_catalog, catalog]) - - joined_catalog.write('catalog.fits', format='fits', overwrite=True) - else: - catalog.write('catalog.fits', format='fits', overwrite=True) - - return - -def cdbs_PHOENIXv16(path_to_cdbs_dir): - """ - Put the PHOENIXv16 (Husser+13) fits files into cdbs format. This primarily - consists of adjusting the flux units from [erg/s/cm^2/cm] to [erg/s/cm^2/A] - and adding the appropriate keywords to the fits header. - - path_to_cdbs_dir goes from current working directory to phoenix[met] directory - in cdbs/grids/phoenix_v16. Note that these files have already been organized - using organize_PHOENIXv16_atmospheres code. - - Overwrites original files in directory - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_cdbs_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('phoenix*.fits') - - ## Need to sort filenames; glob doesn't always give them in order - files.sort() - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - - # Read in current FITS table - cur_table = Table.read(i, format='fits') - - cur_table.columns[0].name = 'Wavelength' - - num_cols = len(cur_table.colnames) - - # Multiplying each flux column by 10^-8 for conversion - for cur_col_index in range(1, num_cols, 1): - cur_col_name = cur_table.colnames[cur_col_index] - cur_table[cur_col_name] = cur_table[cur_col_name] * 10.**-8 - - - # Construct new FITS file based on old one - hdu = fits.open(i) - header_0 = hdu[0].header - header_1 = hdu[1].header - sci = hdu[1].data - - tbhdu = fits.table_to_hdu(cur_table) - - # Copying over the older headers, adding unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - tbhdu.header['TUNIT3'] = 'FLAM' - tbhdu.header['TUNIT4'] = 'FLAM' - tbhdu.header['TUNIT5'] = 'FLAM' - tbhdu.header['TUNIT6'] = 'FLAM' - tbhdu.header['TUNIT7'] = 'FLAM' - tbhdu.header['TUNIT8'] = 'FLAM' - tbhdu.header['TUNIT9'] = 'FLAM' - tbhdu.header['TUNIT10'] = 'FLAM' - tbhdu.header['TUNIT11'] = 'FLAM' - tbhdu.header['TUNIT12'] = 'FLAM' - tbhdu.header['TUNIT13'] = 'FLAM' - tbhdu.header['TUNIT14'] = 'FLAM' - - # Construct and write out final FITS file - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i, overwrite=True) - - hdu.close() - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Change back to starting directory - os.chdir(start_dir) - - return - -def rebin_phoenixV16(cdbs_path): - """ - Rebin phoenixV16 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: phoenix_v16_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - ## (This assumes that at least 'm00' metallicity exists) - tmp = '{0}/grid/phoenix_v16/phoenix{1}/phoenix{1}_02400.fits'.format(cdbs_path, 'm00') - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/phoenix_v16_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/phoenix_v16/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - temp_arr = np.zeros(len(files_all), dtype=float) - logg_arr = np.zeros(len(files_all), dtype=float) - metal_arr = np.zeros(len(files_all), dtype=float) - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - - temp_arr[ff] = float(vals[0]) - metal_arr[ff] = float(vals[1]) - logg_arr[ff] = float(vals[2]) - - - metal_uniq = np.unique(metal_arr) - temp_uniq = np.unique(temp_arr) - - for mm in range(len(metal_uniq)): - metal = metal_uniq[mm] # metallicity - - # Construct str for metallicity (for appropriate directory name) - met_str = str(int(np.abs(metal))) + str(int((metal % 1.0)*10)) - if metal > 0: - met_str = 'p' + met_str - else: - met_str = 'm' + met_str - - # Make directory for current metallicity if it does not exist yet - if not os.path.exists(path + 'phoenix' + met_str): - os.mkdir(path + 'phoenix' + met_str) - - for tt in range(len(temp_uniq)): - temp = temp_uniq[tt] # temperature - - # Pick out the list of gravities for this T, Z combo - idx = np.where((metal_arr == metal) & (temp_arr == temp))[0] - logg_exist = logg_arr[idx] - - # All gravities will go in one file. Here is the output - # file name. - outfile = path + files_all[idx[0]].split('[')[0] - - ## If the rebinned file already exists, continue - if os.path.exists(outfile): - continue - - # Build a columns array. One column for each gravity. - cols_arr = [] - - # Make the wavelength column, which is first in the cols array. - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - cols_arr.append(c0) - - for gg in range(len(logg_exist)): - grav = logg_exist[gg] # gravity - - # Fetch the spectrum - sp = pysynphot.Icat('phoenix_v16', temp, metal, grav) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Store the spectrum - name = 'g{0:3.1f}'.format(grav) - col = fits.Column(name=name, format='E', array=flux_rebin) - cols_arr.append(col) - - - # Make the FITS file from the columns with header. - cols = fits.ColDefs(cols_arr) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - for gg in range(len(logg_exist)): - tbhdu.header['TUNIT{0:d}'.format(gg+2)] = 'FLAM' - - # Write hdu - finalhdu = fits.HDUList([prihdu, tbhdu]) - # don't have overwrite to protect original files. - finalhdu.writeto(outfile) - - print( 'Finished file ' + outfile + ' with gravities: ', logg_exist) - - - return - - -def rebin_spec(wave, specin, wavnew): - """ - Helper routine to rebin spectra. TAKEN FROM ASTROBETTER BLOG FROM JESSICA: - http://www.astrobetter.com/blog/2013/08/12/ - python-tip-re-sampling-spectra-with-pysynphot/ - """ - spec = pysynphot.spectrum.ArraySourceSpectrum(wave=wave, flux=specin) - f = np.ones(len(wave)) - filt = pysynphot.spectrum.ArraySpectralElement(wave, f, waveunits='angstrom') - obs = pysynphot.observation.Observation(spec, filt, binset=wavnew, force='taper') - - return obs.binflux - -def organize_BTSettl_2015_atmospheres(path_to_dir): - """ - Construct cdbs-ready BTSettl_CIFITS_2011_2015 atmospheres for each model. - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/grid/BTSettl_2015 - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the BTSettl subdirectory - if not os.path.exists('BTSettl_2015'): - os.mkdir('BTSettl_2015') - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.spec.fits') - - for i in files: - hdu = fits.open(i) - spec = hdu[1].data - header_0 = hdu[0].header - header_1 = hdu[1].header - - wave = spec.field(0) - flux = spec.field(1) - - # Get units right: convert wave from microns to Angstroms, - # flux from W /m^2/ micron to erg/s/cm^2/A - wave_new = wave * 10**4 - flux_new = flux * 10**(-1) - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave_new) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'grid/BTSettl_2015/'+i, overwrite=True) - - hdu.close() - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_BTSettl_2015_catalog(path_to_dir): - """ - Create cdbs catalog.fits of BTSettl_CIFITS2011_2015 grid. - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*spec.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('-') - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl_2015(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin BTSettle_CIFITS2011_2015 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: BTSettl_2015_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - tmp = cdbs_path+'/grid/phoenix_v16/phoenixm00/phoenixm00_02400.fits' - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - phoenix_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/BTSettl_2015_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/BTSettl_2015/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning BTSettl spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - sp = pysynphot.Icat('BTSettl_2015', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - -def make_wavelength_unique(files, dirname): - """ - Helper function to go through each BTSettl spectrum and ensure that - each wavelength point is unique. This is required for rebinning to work. - - - files: list of files to run this analysis on - """ - # Loop through each file, find fix repeated wavelength entries if necessary - for i in files: - t = Table.read('{0}/{1}'.format(dirname,i), format='fits') - test = np.unique(t['Wavelength'], return_index=True) - - if len(t) != len(test[0]): - t = t[test[1]] - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - # Also make sure wavelength is monotonic. If it is not, then it is - # a sign that the wavelengths are out of order - diff = np.diff(t['Wavelength']) - bad = np.where(diff < 0) - if len(bad[0]) > 0: - t.sort('Wavelength') - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - print('Done {0}'.format(i)) - - return - -def organize_BTSettl_atmospheres(): - """ - Construct cdbs-ready atmospheres for the BTSettl grid (CIFITS2011). - The code expects tp be run in cdbs/grid/BTSettl, and expects that the - individual model files have been downloaded from online - (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011/SPECTRA/) - and processed into python-readable ascii files. - """ - orig_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btm10', 'btm05', 'btp00', 'btp05'] - - - # Go through each directory, turning each spectrum into a cdbs-ready file. - # Will convert flux into Ergs/sec/cm**2/A (FLAM) units and save as a fits file, - # for faster access later - for ii in dirs: - print('Starting {0}'.format(ii)) - os.chdir(ii) - - files = glob.glob('*.txt') - count=0 - for jj in files: - t = Table.read(jj, format='ascii') - # First, trim the wavelengths to a more reasonable wavelength range - good = np.where( (t['col1'] > 1000) & (t['col1'] < 70000) ) - t = t[good] - - # Convert flux units to Flam (Ergs/sec/cm**2/A) - flux_new = 10**(t['col2'] - 8.0) - - # Save the file as a fits file - c0 = fits.Column(name='Wavelength', format='D', array=t['col1']) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Add unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto('{0}.fits'.format(jj[:-4]), overwrite=True) - hdu_new.close() - count += 1 - print('Done {0} of {1}'.format(count, len(files))) - - # Now, clean up all the files made when unzipping the spectra - cmd1 = 'rm *.bz2' - cmd2 = 'rm *.tmp' - #cmd3 = 'rm *.txt' - os.system(cmd1) - os.system(cmd2) - #os.system(cmd3) - print('==============================') - print('Done {0}'.format(ii)) - print('==============================') - - # Go back to original directory, move to next metallicity directory - os.chdir(orig_dir) - - return - -def make_BTSettl_catalog(): - """ - Create cdbs catalog.fits of BTSettl grid. - THIS IS STEP 2, after organize_BTSettl_atmospheres has - been run. - - Code expects to be run in cdbs/grid/BTSettl - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btp05'] - - # Construct the catalog.fits file input. The input consists of - # and index string that specifies the stellar paramters, and a - # name string that points to the file - # Loop over all the metallicity directories to construct these inputs - index_str = [] - name_str = [] - for ii in dirs: - os.chdir(ii) - files = glob.glob('*.fits') - - # Construct the metallicity val - if 'm' in ii: - metal_flag = -1 * float(ii[3:])*0.1 - else: - metal_flag = float(ii[3:])*0.1 - - # Now collect the info from the files - for jj in files: - tmp = jj.split('-') - - if metal_flag >= 0: - temp = float(tmp[0].split('+')[0][3:]) * 100.0 # In kelvin - try: - logg = float(tmp[1]) - except: - logg = float(tmp[1].split('+')[0]) - else: - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0},{1},{2:3.2f}'.format(int(temp), metal_flag, logg)) - name_str.append('{0}/{1}[Flux]'.format(ii, jj)) - - # Go back to original directory to move to next metallicity - print('Done {0}'.format(ii)) - os.chdir(start_dir) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl(make_unique=False): - """ - Rebin BTSettle models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory: BTSettl_rebin - - Code expects to be run in cdbs/grid directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Create cdbs/grid directory for rebinned models - path = 'BTSettl_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata('BTSettl/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - #==============================# - #tmp = [] - #for ii in files_all: - # if ii.startswith('btp00'): - # tmp.append(ii) - #files_all = tmp - #=============================# - - print( 'Rebinning BTSettl spectra') - if make_unique: - print('Making unique') - make_wavelength_unique(files_all, 'BTSettl') - print('Done') - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - try: - sp = pysynphot.Icat('BTSettl', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - except: - pdb.set_trace() - orig_file = '{0}/{1}'.format('BTSettl/', files_all[ff].split('[')[0]) - outfile = path + files_all[ff].split('[')[0] - cmd = 'cp {0} {1}'.format(orig_file, outfile) - os.system(cmd) - - print('Done {0} of {1}'.format(ff, len(files_all))) - - return - -def organize_all_Meisner2023_atmospheres(): - """ - Construct cdbs-ready atmospheres for the Meisner2023 grid. - The code expects tp be run in cdbs/grid/Meisner2023, and expects that the - individual model files have been downloaded from online - and processed into python-readable ascii files. - """ - orig_dir = os.getcwd() - dirs = ['mm10', 'mm05', 'mp00', 'mp03'] - - # Go through each directory, turning each spectrum into a cdbs-ready file. - # Save as a fits file, for faster access later - for ii in dirs: - print('Starting {0}'.format(ii)) - os.chdir(ii) - - files = glob.glob('*.fits') - count=0 - for jj in files: - # Open each .fits file and read the data - with fits.open(jj) as hdul: - data = hdul[1].data - wavelength = data['Wavelength'] - flux = data['Flux'] - - # Make flux independent of R&D - flux_new = flux / 5e-20 - - # Create new columns with desired format - c0 = fits.Column(name='Wavelength', format='D', array=wavelength) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Add unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write the new fits table in the cdbs directory - output_filename = '{0}.fits'.format(jj[:-5]) # Removing the original .fits extension - hdu_new.writeto(output_filename, overwrite=True) - hdu_new.close() - count += 1 - print('Done {0} of {1}'.format(count, len(files))) - - # Go back to original directory, move to next metallicity directory - os.chdir(orig_dir) - - return - -def make_Meisner2023_catalog(): - """ - Create cdbs catalog.fits of Meisner2023 grid. - THIS IS STEP 2, after organize_Meisner2023_atmospheres has - been run. - - Code expects to be run in cdbs/grid/Meisner2023 - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - dirs = ['mm10', 'mm05', 'mp00', 'mp03'] - - # Construct the catalog.fits file input. The input consists of - # and index string that specifies the stellar paramters, and a - # name string that points to the file - # Loop over all the metallicity directories to construct these inputs - index_str = [] - name_str = [] - for ii in dirs: - os.chdir(ii) - files = glob.glob('spec_jwst_*.fits') - - for jj in files: - # Parse temperature, log(g), and metallicity from filename - temp_str = jj.split('_')[2] - logg_str = jj.split('_')[3] - metal_str = jj.split('_')[4] - - # Extract temperature, surface gravity, and metallicity - temp = float(temp_str[1:]) # Temperature in Kelvin - logg = float(logg_str[1:]) # Surface gravity log(g) - - # Build metallicity value - if metal_str.startswith('m'): - metallicity = -1 * float(metal_str[1:]) - else: - metallicity = float(metal_str[1:]) - - # Construct index and filename strings - index_str.append('{0},{1},{2:3.2f}'.format(int(temp), metallicity, logg)) - name_str.append('{0}/{1}[Flux]'.format(ii, jj)) - - print('Processed directory:', ii) - os.chdir(start_dir) - - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_Meisner2023(make_unique=False): - """ - Rebin Meisner2023 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory: Meisner2023_rebin - - Code expects to be run in cdbs/grid directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Create a directory for rebinned Meisner2023 models - rebin_path = 'Meisner2023_rebin/' - if not os.path.exists(rebin_path): - os.mkdir(rebin_path) - - # Load the catalog.fits file and extract all spectra file paths - cat = Table.read('Meisner2023/catalog.fits') - files_all = [cat[ii]['FILENAME'].split('[')[0] for ii in range(len(cat))] - - print('Rebinning Meisner2023 spectra') - if make_unique: - print('Making unique') - make_wavelength_unique(files_all, 'Meisner2023') - print('Done') - - for ff, file in enumerate(files_all): - vals = cat[ff]['INDEX'].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the Meisner2023 spectrum and rebin its flux - try: - sp = pysynphot.Icat('Meisner2023', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Create the output FITS file - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - # Write the new rebinned file in the Meisner2023_rebin directory - outfile = os.path.join(rebin_path, os.path.basename(file)) - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - except Exception as e: - print(f"Error processing {file}: {e}") - orig_file = os.path.join('Meisner2023', file) - outfile = os.path.join(rebin_path, os.path.basename(file)) - os.system(f'cp {orig_file} {outfile}') - - print('Done {0} of {1}'.format(ff + 1, len(files_all))) - - return - - - -def organize_WDKoester_atmospheres(path_to_dir): - """ - Construct cdbs-ready wdKoester WD atmospheres for each model. (from Koester 2010) - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/wdKoeseter - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.dk.dat.txt') - - for i in files: - data = Table.read(i, format='ascii') - - wave = data['col1'] # angstrom - flux = data['col2'] # erg/s/cm^2/A - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'/grid/wdKoester/'+i.replace('.txt', '.fits'), overwrite=True) - - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_WDKoester_catalog(path_to_dir): - """ - Create cdbs catalog.fits of wdKoester grid. - THIS IS STEP 2, after organize_WDKoester_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*dk.dat.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('.') - tmp2 = tmp[0].split('_') - temp = float(tmp2[0][2:]) # Kelvin - logg = float(tmp2[1]) / 100.0 # log(g) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_WDKoester(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin wdKoester models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: wdKoester_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing model; we will steal the header - tmp = cdbs_path+'/grid/wdKoester/da70000_800.dk.dat.fits' - wdkoester_hdu = fits.open(tmp) - header0 = wdkoester_hdu[0].header - wdkoester_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/wdKoester_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/wdKoester/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning wdKoester spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the wdKoester spectrum, rebin flux - sp = pysynphot.Icat('wdKoester', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - - diff --git a/spisea/atmospheres_BASE_58456.py b/spisea/atmospheres_BASE_58456.py deleted file mode 100644 index 8df2fbe1..00000000 --- a/spisea/atmospheres_BASE_58456.py +++ /dev/null @@ -1,2165 +0,0 @@ -import logging -import numpy as np -import pysynphot -import os -import glob -from astropy.io import fits -from astropy.table import Table, Column -import pysynphot -import time -import pdb -import warnings - -log = logging.getLogger('atmospheres') - -def get_atmosphere_bounds(model_dir, metallicity=0, temperature=20000, gravity=4): - """ - Given atmosphere model, get temperature and gravity bounds - """ - # Open catalog fits file and break out row indices - catalog = Table.read('{0}/grid/{1}/catalog.fits'.format(os.environ['PYSYN_CDBS'], model_dir)) - - teff_arr = [] - z_arr = [] - logg_arr = [] - for cur_row_index in range(len(catalog)): - index = catalog['INDEX'][cur_row_index] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Filter by metallicity. Will chose the closest metallicity to desired input - metal_list = np.unique(np.array(z_arr)) - metal_idx = np.argmin(np.abs(metal_list - metallicity)) - - z_filt = np.where(z_arr == metal_list[metal_idx]) - teff_arr = teff_arr[z_filt] - logg_arr = logg_arr[z_filt] - - # # Now find the closest atmosphere in parameter space to - # # the one we want. We'll find the match with the lowest - # # fractional difference - # teff_diff = (teff_arr - temperature) / temperature - # logg_diff = (logg_arr - gravity) / gravity - # - # diff_tot = abs(teff_diff) + abs(logg_diff) - # idx_f = np.argmin(diff_tot) - # - # temperature_new = teff_arr[idx_f] - # gravity_new = logg_arr[idx_f] - - # First check if temperature within bounds - temperature_new = temperature - if temperature > np.max(teff_arr): - temperature_new = np.max(teff_arr) - if temperature < np.min(teff_arr): - temperature_new = np.min(teff_arr) - - # If temperature within bounds, then check if metallicity within bounds - teff_diff = np.abs(teff_arr - temperature) - sorted_min_diffs = np.unique(teff_diff) - - ## Find two closest temperatures - teff_close_1 = teff_arr[np.where(teff_diff == sorted_min_diffs[0])[0][0]] - teff_close_2 = teff_arr[np.where(teff_diff == sorted_min_diffs[1])[0][0]] - - logg_arr_1 = logg_arr[np.where(teff_arr == teff_close_1)] - logg_arr_2 = logg_arr[np.where(teff_arr == teff_close_2)] - - ## Switch to most conservative bound of logg out of two closest temps - gravity_new = gravity - if gravity > np.min([np.max(logg_arr_1), np.max(logg_arr_2)]): - gravity_new = np.min([np.max(logg_arr_1), np.max(logg_arr_2)]) - if gravity < np.max([np.min(logg_arr_1), np.min(logg_arr_2)]): - gravity_new = np.max([np.min(logg_arr_1), np.min(logg_arr_2)]) - - # Print out changes, if any - if temperature_new != temperature: - teff_msg = 'Changing to T={0:6.0f} for T={1:6.0f} logg={2:4.2f}' - print( teff_msg.format(temperature_new, temperature, gravity)) - - if gravity_new != gravity: - logg_msg = 'Changing to logg={0:4.2f} for T={1:6.0f} logg={2:4.2f}' - print( logg_msg.format(gravity_new, temperature, gravity)) - - return (temperature_new, gravity_new) - -def get_kurucz_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmosphere from the Kurucz pysnphot grid - (`Kurucz 1993 `_). - - Grid Range: - - * Teff: 3000 - 50000 K - * gravity: 0 - 5 cgs - * metallicity: -5.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - Always false for this particular function - """ - try: - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('k93models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Kurucz 1993 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_castelli_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmospheres from the pysynphot ATLAS9 atlas - (`Castelli & Kurucz 2004 `_). - - Grid Range: - - * Teff: 3500 - 50000 K - * gravity: 0 - 5.0 cgs - * [M/H]: -2.5 - 0.2 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('ck04models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Castelli and Kurucz 2004 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_nextgen_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - try: - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('nextgen', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find NextGen atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_amesdusty_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - sp = pysynphot.Icat('AMESdusty', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find AMESdusty Allard+ 2000 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenix_atmosphere(metallicity=0, temperature=5000, gravity=4, - rebin=False): - """ - Return atmosphere from the pysynphot - `PHOENIX atlas `_. - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - try: - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIX BT-Settl (Allard+ 2011 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere(metallicity=0, temperature=24000, gravity=4.3, rebin=True): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - # Take care of atmospheres outside the catalog boundaries - logg_msg = 'Changing to logg={0:3.1f} for T={1:6.0f} logg={2:4.2f}' - if gravity > 4.3: - print( logg_msg.format(4.3, temperature, gravity)) - gravity = 4.3 - - if rebin: - sp = pysynphot.Icat('cmfgen_rot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_rot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere_closest(metallicity=0, temperature=24000, gravity=4.3, rebin=True, - verbose=False): - """ - For a given stellar atmosphere, get extract the closest possible match in - Teff/logg space. Note that this is different from the normal routine - which interpolates along the input grid to get final spectrum. We can't - do this here because the Fierro+15 atmosphere grid is so sparse - - rebin=True: pull from atmospheres at ck04model resolution. - - If verbose, print out the parameters of the match - """ - # Set up the proper root directory - if rebin == True: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot_rebin/' - else: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot/' - - # Read in catalog, extract atmosphere info - cat = Table.read('{0}/catalog.fits'.format(root_dir), format='fits') - teff_arr = [] - z_arr = [] - logg_arr = [] - for ii in range(len(cat)): - index = cat['INDEX'][ii] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Now find the closest atmosphere in parameter space to - # the one we want. We'll find the match with the lowest - # fractional difference - teff_diff = (teff_arr - temperature) / temperature - logg_diff = (logg_arr - gravity) / gravity - - diff_tot = abs(teff_diff) + abs(logg_diff) - idx_f = np.where(diff_tot == min(diff_tot))[0][0] - - # Extract the filename of the best-match model and read as - # pysynphot object - infile = cat[idx_f]['FILENAME'].split('.') - spec = Table.read('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Now, the CMFGEN atmospheres assume a distance of 1 kpc, while the the - # ATLAS models are in FLAM at the surface. So, we need to multiply the - # CMFGEN atmospheres by (1000/R)**2. in order to convert to FLAM on surface. - # We'll calculate radius from Teff and logL, which is given in the Table_*.txt file - t = Table.read('{0}/Table_rot.txt'.format(root_dir), format='ascii') - tmp = np.where(t['col1'] == infile[0]) - - lum = t['col3'][tmp] * (3.839*10**33) # cgs - sigma = 5.6704 * 10**-5 # cgs - teff = teff_arr[idx_f] # cgs - - radius = np.sqrt( lum / (4.0 * np.pi * teff**4. * sigma) ) # in cm - radius /= 3.08*10**18 # in pc - - - # Make the pysynphot spectrum - w = spec['Wavelength'] - f = spec['Flux'] * (1000 / radius)**2. - sp = pysynphot.ArraySpectrum(w,f) - - #sp = pysynphot.FileSpectrum('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Print out parameters of match, if desired - if verbose: - print('Teff match: Input: {0}, Output: {1}'.format(temperature, teff_arr[idx_f])) - print('logg match: Input: {0}, Output: {1}'.format(gravity, logg_arr[idx_f])) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=22500, gravity=3.98, rebin=True): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - if rebin: - sp = pysynphot.Icat('cmfgen_norot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_norot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=30000, gravity=4.14): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 30000) - gravity = log gravity (def = 4.14) - """ - sp = pysynphot.Icat('cmfgenF15_noRot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN non-rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenixv16_atmosphere(metallicity=0, temperature=4000, gravity=4, rebin=True): - """ - Return PHOENIX v16 atmospheres from - `Husser et al. 2013 `_. - - Models originally downloaded via `ftp `_. - Solar metallicity and [alpha/Fe] is used. - - Grid Range: - - * Teff: 2300 - 7000 K, steps of 100 K; 7000 - 12000 in steps of 200 K - * gravity: 0.0 - 6.0 cgs, steps of 0.5 - * [M/H]: -4.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - atm_model_name = 'phoenix_v16' - if rebin == True: - atm_model_name = 'phoenix_v16_rebin' - - - # Extract atmosphere. If that fails, then check bounds and try again - try: - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_model_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIXv16 (Husser+13) atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_2015_atmosphere(metallicity=0, temperature=2500, gravity=4, rebin=True): - """ - Return atmosphere from CIFIST2011_2015 grid - (`Allard et al. 2012 `_, - `Baraffe et al. 2015 `_ ) - - Grid originally downloaded from `website `_. - - Grid Range: - - * Teff: 1200 - 7000 K - * gravity: 2.5 - 5.5 cgs - * [M/H] = 0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - if rebin == True: - atm_name = 'BTSettl_2015_rebin' - else: - atm_name = 'BTSettl_2015' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl_2015 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_atmosphere(metallicity=0, temperature=2500, gravity=4.5, rebin=True): - """ - Return atmosphere from CIFIST2011 grid - (`Allard et al. 2012 `_) - - Grid originally downloaded `here `_ - - Notes - ------ - Grid Range: - - * [M/H] = -2.5, -2.0, -1.5, -1.0, -0.5, 0, 0.5 - - Teff and gravity ranges depend on metallicity: - - [M/H] = -2.5 - - * Teff: 2600 - 4600 K - * gravity: 4.5 - 5.5 - - [M/H] = -2.0 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.5 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.0 - - * Teff: 2600 - 7000 - * gravity: Teff < 3200 --> 4.5 - 5.5; Teff > 3200 --> 2.5 - 5.5 - - [M/H] = -0.5 - - * Teff: 1000 -7000 - * gravity: Teff < 3000 --> 4.5 - 5.5; Teff > 3000 --> 3.0 - 6.0 - - [M/H] = 0 - - * Teff: 750 - 7000 - * gravity: Teff < 2500 --> 3.5 - 5.5; Teff > 2500 --> 0 - 5.5 - - [M/H] = 0.5 - - * Teff: 1000 - 5000 - * gravity: 3.5 - 5.0 - - - Alpha enhancement: - - * [M/H]= -0.0, +0.5 no anhancement - * [M/H]= -0.5 with [alpha/H]=+0.2 - * [M/H]= -1.0, -1.5, -2.0, -2.5 with [alpha/H]=+0.4 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - if rebin == True: - atm_name = 'BTSettl_rebin' - else: - atm_name = 'BTSettl' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl_2015 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_wdKoester_atmosphere(metallicity=0, temperature=20000, gravity=7): - """ - Return white dwarf atmospheres from - `Koester et al. 2010 `_ - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - sp = pysynphot.Icat('wdKoester', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find WD Koester (Koester+ 2010 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_atlas_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of atlas ck04 model and phoenixV16. - - Only valid for temps between 5000 - 5500K, gravity from 0 = 5.0 - """ - try: - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('merged_atlas_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model - and phoenixV16. - - Only valid for temps between 3200 - 3800K, gravity from 2.5 - 5.5 - """ - try: - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('merged_BTSettl_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -#---------------------------------------------------------------------# -def get_merged_atmosphere(metallicity=0, temperature=20000, gravity=4.5, verbose=False, - rebin=True): - """ - Return a stellar atmosphere from a suite of different model grids, - depending on the input temperature, (all values in K). - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - - Notes - ----- - The underlying stellar model grid used changes as a function of - stellar temperature (in K): - - * T > 20,000: ATLAS - * 5500 <= T < 20,000: ATLAS - * 5000 <= T < 5500: ATLAS/PHOENIXv16 merge - * 3800 <= T < 5000: PHOENIXv16 - - For T < 3800, there is an additional gravity and metallicity - dependence: - - If T < 3800 and [M/H] = 0: - - * T < 3800, logg < 2.5: PHOENIX v16 - * 3200 <= T < 3800, logg > 2.5: BTSettl_CIFITS2011_2015/PHOENIXV16 merge - * 3200 < T <= 1200, logg > 2.5: BTSettl_CIFITS2011_2015 - - Otherwise, if T < 3800 and [M/H] != 0: - - * T < 3800: PHOENIX v16 - - References: - - * ATLAS: ATLAS9 models (`Castelli & Kurucz 2004 `_) - * PHOENIXv16 (`Husser et al. 2013 `_) - * BTSettl_CIFITS2011_2015: Baraffee+15, Allard+ (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011_2015/SPECTRA/) - - LTE WARNING: - - The ATLAS atmospheres are calculated with LTE, and so they - are less accurate when non-LTE conditions apply (e.g. T > 20,000 - K). Ultimately we'd like to add a non-LTE atmosphere grid for - the hottest stars in the future. - - HOW BOUNDARIES BETWEEN MODELS ARE TREATED: - - At the boundary between two models grids a temperature range is defined - where the resulting atmosphere is a weighted average between the two - grids. Near one boundary one model - is weighted more heavily, while at the other boundary the other - model is weighted more heavily. These are calculated in the - temperature ranges where we switch between model grids, to - ensure a smooth transition. - """ - # For T < 3800, atmosphere depends on metallicity + gravity. - # If solar metallicity, use BTSettl 2015 grid. Only solar metallicity is - # currently available here, so if non-solar metallicity, just stick with - # the Phoenix grid - if (temperature <= 3800) & (metallicity == 0): - # High gravity are in BTSettl regime - if (temperature <= 3200) & (gravity > 2.5): - if verbose: - print( 'BTSettl_2015 atmosphere') - return get_BTSettl_2015_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 3200) & (temperature < 3800) & (gravity > 2.5): - if verbose: - print( 'BTSettl/Phoenixv16 merged atmosphere') - return get_BTSettl_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - # Low gravity is PHOENIX regime - if gravity <= 2.5: - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 3800) & (metallicity != 0): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - # For T > 3800, no metallicity or gravity dependence - if (temperature >= 3800) & (temperature < 5000): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 5000) & (temperature < 5500): - if verbose: - print( 'ATLAS/Phoenix merged atmosphere') - return get_atlas_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if (temperature >= 5500) & (temperature < 20000): - if verbose: - print( 'ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if temperature >= 20000: - if verbose: - print( 'Still ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - #print('CMFGEN') - #return get_cmfgenRot_atmosphere_closest(metallicity=metallicity, - # temperature=temperature, - # gravity=gravity) - - - - -def get_wd_atmosphere(metallicity=0, temperature=20000, gravity=4, verbose=False): - """ - Return the white dwarf atmosphere from - `Koester et al. 2010 `_. - If desired parameters are - outside of grid, return a blackbody spectrum instead - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - if verbose: - print('wdKoester atmosphere') - - return get_wdKoester_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - except pysynphot.exceptions.ParameterOutOfBounds: - # Use a black-body atmosphere. - bbspec = get_bb_atmosphere(temperature=temperature, verbose=verbose) - return bbspec - -def get_bb_atmosphere(metallicity=None, temperature=20_000, gravity=None, - verbose=False, rebin=None, - wave_min=500, wave_max=50_000, wave_num=20_000): - """ - Return a blackbody spectrum - - Parameters - ---------- - temperature: float, default=20_000 - The stellar temperature, in units of K - wave_min: float, default=500 - Sets the minimum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_max: float, default=50_000 - Sets the maximum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_num: int, default=20_000 - Sets the number of wavelength points in the wavelength range - Note: the wavelength range is evenly spaced in log space - """ - if ((metallicity is not None) or (gravity is not None) or - (rebin is not None)): - warnings.warn( - 'Only `temperature` keyword is used for black-body atmosphere' - ) - - if verbose: - print('Black-body atmosphere') - - # Modify pysynphot's default waveset to specified bounds - pysynphot.refs.set_default_waveset( - minwave=wave_min, maxwave=wave_max, num=wave_num - ) - - # Get black-body atmosphere for specified temperature from pysynphot - bbspec = pysynphot.spectrum.BlackBody(temperature) - - # pysynphot `BlackBody` generates spectrum in `photlam`, need in `flam` - bbspec.convert('flam') - - # `BlackBody` spectrum is normalized to solar radius star at 1 kiloparsec. - # Need to remove this normalization for SPISEA by multiplying bbspec - # by (1000 * 1 parsec / 1 Rsun)**2 = (1000 * 3.08e18 cm / 6.957e10 cm)**2 - bbspec *= (1000 * 3.086e18 / 6.957e10)**2 - - return bbspec - - -#--------------------------------------# -# Atmosphere formatting functions -#--------------------------------------# - -def download_CMFGEN_atmospheres(Table_rot, Table_norot): - """ - Downloads CMFGEN models from - https://sites.google.com/site/fluxesandcontinuum/home; - these contain continuum as well as lines. - - Table_rot, Table_norot are tables with the file prefixes - and model atmosphere parameters, taken by hand from the - Fierro+15 paper - - Website addresses are hardcoded - - Puts downloaded models in the current working directory. - """ - print( 'WARNING: THIS DOES NOT COMPLETELY WORK') - print( '**********************') - t_rot = Table.read(Table_rot, format='ascii') - t_norot = Table.read(Table_norot, format='ascii') - - tables = [t_rot, t_norot] - filenames = [t_rot['col1'], t_norot['col1']] - - # Hardcoded list of webiste addresses - web_base1 = 'https://sites.google.com/site/fluxesandcontinuum/home/' - web_base2 = 'https://sites.google.com/site/modelsobmassivestars/' - web = [web_base1+'009-solar-masses/',web_base1+'012-solar-masses/', - web_base1+'015-solar-masses/',web_base1+'020-solar-masses/', - web_base1+'025-solar-masses/',web_base2+'009-solar-masses-tracks/', - web_base2+'040-solar-masses/',web_base2+'060-solar-masses/', - web_base1+'085-solar-masses/',web_base1+'120-solar-masses/'] - # Array of masses that matches the website addresses - mass_arr = np.array([9.,12.,15.,20.,25.,32.,40.,60.,85.,120.]) - - # Loop through rotating and unrotating case. First loop is rot, second unrot - for i in range(2): - # Extract masses from filenames - masses = [] - for j in filenames[i]: - tmp = j.split('m') - mass = float(tmp[1][:-1]) - masses.append(mass) - - # Download the models webpage by webpage. A bit tricky because masses - # change slightly within a particular website. THIS IS WHAT FAILS - for j in range(len(web)): - if j == 0: - good = np.where( (masses <= mass_arr[j]) ) - else: - g = j - 1 - good = np.where( (masses <= mass_arr[j]) & - (masses > mass_arr[g]) ) - # Use wget command to pull down the files, and unzip them - for k in good[0]: - full = web[j]+'{1:s}.flx.zip'.format(mass_arr[j],filenames[i][k]) - os.system('wget ' + full) - os.system('unzip '+ filenames[i][k] + '.flx.zip') - - return - -def organize_CMFGEN_atmospheres(path_to_dir): - """ - Organize CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html) - into rot and noRot directories - - path_to_dir is from current working directory to directory - containing the downloaded models. Assumed that models - and tables describing parameters are in this directory. - - Tables describing parameters MUST be named Table_rot.txt, - Table_noRot.txt. Made by hand from Tables 3, 4 in Fierro+15. - These are located in same original directory as atmosphere files - - Will separate files into 2 subdirectories, one rotating and - the other non-rotating - - *Can't have any other files starting with "t" in model directory to start!* - """ - # First, record current working directory to return to later - start_dir = os.getcwd() - - # Enter atmosphere directory, collect rotating and non-rotating - # file names (assumed to all start with "t") - os.chdir(path_to_dir) - rot_models = glob.glob("t*r.flx*") - noRot_models = glob.glob("t*n.flx*") - - # Separate into different subdirectories - if os.path.exists('cmfgenF15_rot'): - pass - else: - os.mkdir('cmfgenF15_rot') - os.mkdir('cmfgenF15_noRot') - - for mod in rot_models: - cmd = 'mv {0:s} cmfgenF15_rot'.format(mod) - os.system(cmd) - - for mod in noRot_models: - cmd = 'mv {0:s} cmfgenF15_noRot'.format(mod) - os.system(cmd) - - # Also move Tables with model parameters into correct directory - os.system('mv Table_rot.txt cmfgenF15_rot') - os.system('mv Table_noRot.txt cmfgenF15_noRot') - - # Return to original directory - os.chdir(start_dir) - - return - -def make_CMFGEN_catalog(path_to_dir): - """ - Create cdbs catalog.fits of CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html). - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to directory - containing the rotating or non-rotating models (i.e. cmfgenF15_rot). Also, - needs to be a Table*.txt file which contains the parameters for all of the - original models, since params in filename are not precise enough - - Will create catalog.fits file in atmosphere directory with - description of each model - - *Can't have any other files starting with "t" in model directory to start!* - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere - # Note: can't rely on filename for this because not precise enough!! - - #---------OLD: GETTING PARAMS FROM FILENAME-------# - # Collect file names (assumed to all start with "t") - #files = glob.glob("t*") - #for name in files: - # tmp = name.split('l') - # temp = float(tmp[0][1:]) * 100.0 # In kelvin - - # lumtmp = tmp[1].split('_') - # lum = float(lumtmp[0][:-5]) * 1000.0 # In L_sun - - # mass = float(lumtmp[0][5:-1]) # In M_sun - - # Need to calculate log g from T and L (cgs) - # lum_sun = 3.846 * 10**33 # erg/s - # M_sun = 2 * 10**33 # g - # G_si = 6.67 * 10**(-8) # cgs - # sigma_si = 5.67 * 10**(-5) # cgs - - # g = (G_si * mass * M_sun * 4 * np.pi * sigma_si * temp**4) / \ - # (lum * lum_sun) - # logg = np.log10(g) - #---------------------------------------------------# - - # Read table with atmosphere params - table = glob.glob('Table_*') - t = Table.read(table[0], format = 'ascii') - names = t['col1'] - temps = t['col2'] - logg = t['col4'] - - # Create catalog.fits file - index_str = [] - name_str = [] - for i in range(len(names)): - index = '{0:5.0f},0.0,{1:3.2f}'.format(temps[i], logg[i]) - - #---NOTE: THE FOLLOWING DEPENDS ON FINAL LOCATION OF CATALOG FILE---# - #path = path_to_dir + '/' + names[i] - path = names[i] + '.fits[Flux]' - - index_str.append(index) - name_str.append(path) - - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits') - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def cdbs_cmfgen(path_to_dir, path_to_cdbs_dir): - """ - Code to put cmfgen models into cdbs format and adds proper unit keyword in - fits header. Save as fits file - - path_to_dir goes from current directory to cmfgen_rot or cmfgen_norot - directory with the *.flx models. Note that these files have already been - organized using organize_CMFGEN_atmospheres code. - - path_to_cdbs_dir goes from current directory to cdbs/grid/cmfgen_rot or - cmfgen_norot directory. Will copy new fits files to this directory. - This directory must already exist! - """ - # Save starting directory for later, move into path_to_dir directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('*.flx') - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - # Open file, extract useful info - t = Table.read(i, format='ascii') - wave = t['col1'] - flux = t['col2'] # Flux is already in erg/cm^2/s/A - - # Need to eliminate duplicate entries (pysynphot crashes) - unique = np.unique(wave, return_index=True) - wave = wave[unique[1]] - flux = flux[unique[1]] - - # Make fits table from individual columns. - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - #Adding unit keywords - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - prihdu = fits.PrimaryHDU() - - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i[:-4]+'.fits', overwrite=True) - - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Return to original directory, copy over new .fits files to cdbs directory - os.chdir(start_dir) - cmd = 'mv {0:s}/*.fits {1:s}'.format(path_to_dir, path_to_cdbs_dir) - os.system(cmd) - - return - -def rebin_cmfgen(cdbs_path, rot=True): - """ - Rebin cmfgen_rot and cmfgen_norot models to atlas ck04 resolution; - this makes spectrophotometry MUCH faster - - cdbs_path: path to cdbs directory - rot=True for rotating models (cmfgen_rot), False for non-rotating models - - makes new directory in cdbs/grid: cmfgen_rot_rebin or cmfgen_norot_rebin - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing cmfgen model; we will steal the header. - # Also define paths to new rebin directories - if rot == True: - tmp = cdbs_path+'/grid/cmfgen_rot/t0200l0008m009r.fits' - path = cdbs_path+'/grid/cmfgen_rot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_rot/' - else: - tmp = cdbs_path+'/grid/cmfgen_norot/t0200l0007m009n.fits' - path = cdbs_path+'/grid/cmfgen_norot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_norot/' - - cmfgen_hdu = fits.open(tmp) - header0 = cmfgen_hdu[0].header - # Create rebin directories if they don't already exist. Copy over - # catalog.fits file from original directory (will be the same) - if not os.path.exists(path): - os.mkdir(path) - cmd = 'cp {0:s}catalog.fits {1:s}'.format(orig_path, path) - os.system(cmd) - - # Read in the catalog.fits file - cat = fits.getdata(orig_path + 'catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - # First column in new files will be for [atlas] wavelength - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - - # For each catalog.fits entry, read the unbinned spectrum and rebin to - # the atlas resolution. Make a new fits file in rebin directory - count = 0 - for ff in range(len(files_all)): - count += 1 - # Extract the temp, Z, logg - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - grav = float(vals[2]) - - # Fetch the spectrum - if rot == True: - sp = pysynphot.Icat('cmfgen_rot', temp, metal, grav) - else: - sp = pysynphot.Icat('cmfgen_norot', temp, metal, grav) - - # Rebin - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - # Make the FITS file from the columns with header - cols = fits.ColDefs([c0,c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - # Write hdu to new directory with same filename - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(path+files_all[ff]) - - print( 'Finished file {0} of {1}'.format(count, len(files_all)) ) - return - - -def organize_PHOENIXv16_atmospheres(path_to_dir, met_str='m00'): - """ - Construct the Phoenix Husser+13 atmopsheres for each model. Combines the - fluxes from the *HiRES.fits files and the wavelengths of the - WAVE_PHONEIX-ACES-AGSS-COND-2011.fits file. - - path_to_dir is the path to the directory containing all of the downloaded - files - - met_str is the name of the current metallicity - - Creates new fits files for each atmosphere: phoenix_.fits, - which contains columns for the log g (column header = g#.#). Puts - atmospheres in new directory phoenixm00 - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the current metallicity subdirectory - sub_dir = '../phoenix{0}'.format(met_str) - if os.path.exists(sub_dir): - pass - else: - os.mkdir(sub_dir) - - # Extract wavelength array, make column for later - wavefile = fits.open('WAVE_PHOENIX-ACES-AGSS-COND-2011.fits') - wave = wavefile[0].data - wavefile.close() - wave_col = Column(wave, name = 'WAVELENGTH') - - # Create temp array for Husser+13 grid (given in paper) - temp_arr = np.arange(2300, 7001, 100) - temp_arr = np.append(temp_arr, np.arange(7000, 12001, 200)) - - print( 'Looping though all temps') - # For each temp, build file containing the flux for all gravities - i = 0 - for temp in temp_arr: - files = glob.glob('lte{0:05d}-*-HiRes.fits'.format(temp)) - files.sort() - # Start the table with the wavelength column - t = Table() - t.add_column(wave_col) - for f in files: - # Extract the logg out of filename - logg = f[9:13] - - # Extract fluxes from file - spectrum = fits.open(f) - flux = spectrum[0].data - spectrum.close() - - # Make Column object with fluxes, add to table - col = Column(flux, name = 'g{0:2.1f}'.format(float(logg))) - t.add_column(col) - - # Now, construct final fits file for the given temp - outname = 'phoenix{0}_{1:05d}.fits'.format(met_str, temp) - t.write('{0}/{1}'.format(sub_dir, outname), format = 'fits', overwrite = True) - - # Progress counter for user - i += 1 - print( 'Done {0:d} of {1:d}'.format(i, len(temp_arr))) - - # Return to original directory - os.chdir(start_dir) - return - -def make_PHOENIXv16_catalog(path_to_dir, met_str='m00'): - """ - Makes catalog.fits file for Husser+13 phoenix models. Assumes that - organize_PHOENIXv16_atmospheres has been run already, and that the models lie - in subdirectory phoenix[met_str]. - - path_to_directory is the path to the directory with the reformatted - models (i.e. the output from construct_atmospheres, phoenix[met_str]) - - Puts catalog.fits file in directory the user starts in - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Extract metallicity from metallicity string - met = float(met_str[1]) + (float(met_str[2]) * 0.1) - if 'm' in met_str: - met *= -1. - - # Collect the filenames. Each is a unique temp with many different log g's - files = glob.glob('phoenix*.fits') - files.sort() - - # Create the catalog.fits file, row by row - index_arr = [] - filename_arr = [] - for i in files: - # Get log g values from the column header the file - t = Table.read(i, format='fits') - keys = t.keys() - logg_vals = keys[1:] - - # Extract temp from filename - name = i.split('_') - temp = float(name[1][:-5]) - for j in logg_vals: - logg = float(j[1:]) - index = '{0:5.0f},{1:2.1f},{2:2.1f}'.format(temp, met, logg) - filename = path_to_dir + i + '[' + j + ']' - # Add row to table - index_arr.append(index) - filename_arr.append(filename) - - catalog = Table([index_arr, filename_arr], names=('INDEX', 'FILENAME')) - - # Return to starting directory, write catalog - os.chdir(start_dir) - - if os.path.exists('catalog.fits'): - from astropy.table import vstack - - prev_catalog = Table.read('catalog.fits', format='fits') - joined_catalog = vstack([prev_catalog, catalog]) - - joined_catalog.write('catalog.fits', format='fits', overwrite=True) - else: - catalog.write('catalog.fits', format='fits', overwrite=True) - - return - -def cdbs_PHOENIXv16(path_to_cdbs_dir): - """ - Put the PHOENIXv16 (Husser+13) fits files into cdbs format. This primarily - consists of adjusting the flux units from [erg/s/cm^2/cm] to [erg/s/cm^2/A] - and adding the appropriate keywords to the fits header. - - path_to_cdbs_dir goes from current working directory to phoenix[met] directory - in cdbs/grids/phoenix_v16. Note that these files have already been organized - using organize_PHOENIXv16_atmospheres code. - - Overwrites original files in directory - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_cdbs_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('phoenix*.fits') - - ## Need to sort filenames; glob doesn't always give them in order - files.sort() - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - - # Read in current FITS table - cur_table = Table.read(i, format='fits') - - cur_table.columns[0].name = 'Wavelength' - - num_cols = len(cur_table.colnames) - - # Multiplying each flux column by 10^-8 for conversion - for cur_col_index in range(1, num_cols, 1): - cur_col_name = cur_table.colnames[cur_col_index] - cur_table[cur_col_name] = cur_table[cur_col_name] * 10.**-8 - - - # Construct new FITS file based on old one - hdu = fits.open(i) - header_0 = hdu[0].header - header_1 = hdu[1].header - sci = hdu[1].data - - tbhdu = fits.table_to_hdu(cur_table) - - # Copying over the older headers, adding unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - tbhdu.header['TUNIT3'] = 'FLAM' - tbhdu.header['TUNIT4'] = 'FLAM' - tbhdu.header['TUNIT5'] = 'FLAM' - tbhdu.header['TUNIT6'] = 'FLAM' - tbhdu.header['TUNIT7'] = 'FLAM' - tbhdu.header['TUNIT8'] = 'FLAM' - tbhdu.header['TUNIT9'] = 'FLAM' - tbhdu.header['TUNIT10'] = 'FLAM' - tbhdu.header['TUNIT11'] = 'FLAM' - tbhdu.header['TUNIT12'] = 'FLAM' - tbhdu.header['TUNIT13'] = 'FLAM' - tbhdu.header['TUNIT14'] = 'FLAM' - - # Construct and write out final FITS file - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i, overwrite=True) - - hdu.close() - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Change back to starting directory - os.chdir(start_dir) - - return - -def rebin_phoenixV16(cdbs_path): - """ - Rebin phoenixV16 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: phoenix_v16_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - ## (This assumes that at least 'm00' metallicity exists) - tmp = '{0}/grid/phoenix_v16/phoenix{1}/phoenix{1}_02400.fits'.format(cdbs_path, 'm00') - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/phoenix_v16_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/phoenix_v16/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - temp_arr = np.zeros(len(files_all), dtype=float) - logg_arr = np.zeros(len(files_all), dtype=float) - metal_arr = np.zeros(len(files_all), dtype=float) - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - - temp_arr[ff] = float(vals[0]) - metal_arr[ff] = float(vals[1]) - logg_arr[ff] = float(vals[2]) - - - metal_uniq = np.unique(metal_arr) - temp_uniq = np.unique(temp_arr) - - for mm in range(len(metal_uniq)): - metal = metal_uniq[mm] # metallicity - - # Construct str for metallicity (for appropriate directory name) - met_str = str(int(np.abs(metal))) + str(int((metal % 1.0)*10)) - if metal > 0: - met_str = 'p' + met_str - else: - met_str = 'm' + met_str - - # Make directory for current metallicity if it does not exist yet - if not os.path.exists(path + 'phoenix' + met_str): - os.mkdir(path + 'phoenix' + met_str) - - for tt in range(len(temp_uniq)): - temp = temp_uniq[tt] # temperature - - # Pick out the list of gravities for this T, Z combo - idx = np.where((metal_arr == metal) & (temp_arr == temp))[0] - logg_exist = logg_arr[idx] - - # All gravities will go in one file. Here is the output - # file name. - outfile = path + files_all[idx[0]].split('[')[0] - - ## If the rebinned file already exists, continue - if os.path.exists(outfile): - continue - - # Build a columns array. One column for each gravity. - cols_arr = [] - - # Make the wavelength column, which is first in the cols array. - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - cols_arr.append(c0) - - for gg in range(len(logg_exist)): - grav = logg_exist[gg] # gravity - - # Fetch the spectrum - sp = pysynphot.Icat('phoenix_v16', temp, metal, grav) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Store the spectrum - name = 'g{0:3.1f}'.format(grav) - col = fits.Column(name=name, format='E', array=flux_rebin) - cols_arr.append(col) - - - # Make the FITS file from the columns with header. - cols = fits.ColDefs(cols_arr) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - for gg in range(len(logg_exist)): - tbhdu.header['TUNIT{0:d}'.format(gg+2)] = 'FLAM' - - # Write hdu - finalhdu = fits.HDUList([prihdu, tbhdu]) - # don't have overwrite to protect original files. - finalhdu.writeto(outfile) - - print( 'Finished file ' + outfile + ' with gravities: ', logg_exist) - - - return - - -def rebin_spec(wave, specin, wavnew): - """ - Helper routine to rebin spectra. TAKEN FROM ASTROBETTER BLOG FROM JESSICA: - http://www.astrobetter.com/blog/2013/08/12/ - python-tip-re-sampling-spectra-with-pysynphot/ - """ - spec = pysynphot.spectrum.ArraySourceSpectrum(wave=wave, flux=specin) - f = np.ones(len(wave)) - filt = pysynphot.spectrum.ArraySpectralElement(wave, f, waveunits='angstrom') - obs = pysynphot.observation.Observation(spec, filt, binset=wavnew, force='taper') - - return obs.binflux - -def organize_BTSettl_2015_atmospheres(path_to_dir): - """ - Construct cdbs-ready BTSettl_CIFITS_2011_2015 atmospheres for each model. - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/grid/BTSettl_2015 - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the BTSettl subdirectory - if not os.path.exists('BTSettl_2015'): - os.mkdir('BTSettl_2015') - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.spec.fits') - - for i in files: - hdu = fits.open(i) - spec = hdu[1].data - header_0 = hdu[0].header - header_1 = hdu[1].header - - wave = spec.field(0) - flux = spec.field(1) - - # Get units right: convert wave from microns to Angstroms, - # flux from W /m^2/ micron to erg/s/cm^2/A - wave_new = wave * 10**4 - flux_new = flux * 10**(-1) - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave_new) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'grid/BTSettl_2015/'+i, overwrite=True) - - hdu.close() - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_BTSettl_2015_catalog(path_to_dir): - """ - Create cdbs catalog.fits of BTSettl_CIFITS2011_2015 grid. - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*spec.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('-') - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl_2015(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin BTSettle_CIFITS2011_2015 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: BTSettl_2015_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - tmp = cdbs_path+'/grid/phoenix_v16/phoenixm00/phoenixm00_02400.fits' - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - phoenix_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/BTSettl_2015_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/BTSettl_2015/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning BTSettl spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - sp = pysynphot.Icat('BTSettl_2015', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - -def make_wavelength_unique(files, dirname): - """ - Helper function to go through each BTSettl spectrum and ensure that - each wavelength point is unique. This is required for rebinning to work. - - - files: list of files to run this analysis on - """ - # Loop through each file, find fix repeated wavelength entries if necessary - for i in files: - t = Table.read('{0}/{1}'.format(dirname,i), format='fits') - test = np.unique(t['Wavelength'], return_index=True) - - if len(t) != len(test[0]): - t = t[test[1]] - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - # Also make sure wavelength is monotonic. If it is not, then it is - # a sign that the wavelengths are out of order - diff = np.diff(t['Wavelength']) - bad = np.where(diff < 0) - if len(bad[0]) > 0: - t.sort('Wavelength') - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - print('Done {0}'.format(i)) - - return - -def organize_BTSettl_atmospheres(): - """ - Construct cdbs-ready atmospheres for the BTSettl grid (CIFITS2011). - The code expects tp be run in cdbs/grid/BTSettl, and expects that the - individual model files have been downloaded from online - (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011/SPECTRA/) - and processed into python-readable ascii files. - """ - orig_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btm10', 'btm05', 'btp00', 'btp05'] - - - # Go through each directory, turning each spectrum into a cdbs-ready file. - # Will convert flux into Ergs/sec/cm**2/A (FLAM) units and save as a fits file, - # for faster access later - for ii in dirs: - print('Starting {0}'.format(ii)) - os.chdir(ii) - - files = glob.glob('*.txt') - count=0 - for jj in files: - t = Table.read(jj, format='ascii') - # First, trim the wavelengths to a more reasonable wavelength range - good = np.where( (t['col1'] > 1000) & (t['col1'] < 70000) ) - t = t[good] - - # Convert flux units to Flam (Ergs/sec/cm**2/A) - flux_new = 10**(t['col2'] - 8.0) - - # Save the file as a fits file - c0 = fits.Column(name='Wavelength', format='D', array=t['col1']) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Add unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto('{0}.fits'.format(jj[:-4]), overwrite=True) - hdu_new.close() - count += 1 - print('Done {0} of {1}'.format(count, len(files))) - - # Now, clean up all the files made when unzipping the spectra - cmd1 = 'rm *.bz2' - cmd2 = 'rm *.tmp' - #cmd3 = 'rm *.txt' - os.system(cmd1) - os.system(cmd2) - #os.system(cmd3) - print('==============================') - print('Done {0}'.format(ii)) - print('==============================') - - # Go back to original directory, move to next metallicity directory - os.chdir(orig_dir) - - return - -def make_BTSettl_catalog(): - """ - Create cdbs catalog.fits of BTSettl grid. - THIS IS STEP 2, after organize_BTSettl_atmospheres has - been run. - - Code expects to be run in cdbs/grid/BTSettl - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btp05'] - - # Construct the catalog.fits file input. The input consists of - # and index string that specifies the stellar paramters, and a - # name string that points to the file - # Loop over all the metallicity directories to construct these inputs - index_str = [] - name_str = [] - for ii in dirs: - os.chdir(ii) - files = glob.glob('*.fits') - - # Construct the metallicity val - if 'm' in ii: - metal_flag = -1 * float(ii[3:])*0.1 - else: - metal_flag = float(ii[3:])*0.1 - - # Now collect the info from the files - for jj in files: - tmp = jj.split('-') - - if metal_flag >= 0: - temp = float(tmp[0].split('+')[0][3:]) * 100.0 # In kelvin - try: - logg = float(tmp[1]) - except: - logg = float(tmp[1].split('+')[0]) - else: - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0},{1},{2:3.2f}'.format(int(temp), metal_flag, logg)) - name_str.append('{0}/{1}[Flux]'.format(ii, jj)) - - # Go back to original directory to move to next metallicity - print('Done {0}'.format(ii)) - os.chdir(start_dir) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl(make_unique=False): - """ - Rebin BTSettle models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory: BTSettl_rebin - - Code expects to be run in cdbs/grid directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Create cdbs/grid directory for rebinned models - path = 'BTSettl_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata('BTSettl/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - #==============================# - #tmp = [] - #for ii in files_all: - # if ii.startswith('btp00'): - # tmp.append(ii) - #files_all = tmp - #=============================# - - print( 'Rebinning BTSettl spectra') - if make_unique: - print('Making unique') - make_wavelength_unique(files_all, 'BTSettl') - print('Done') - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - try: - sp = pysynphot.Icat('BTSettl', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - except: - pdb.set_trace() - orig_file = '{0}/{1}'.format('BTSettl/', files_all[ff].split('[')[0]) - outfile = path + files_all[ff].split('[')[0] - cmd = 'cp {0} {1}'.format(orig_file, outfile) - os.system(cmd) - - print('Done {0} of {1}'.format(ff, len(files_all))) - - return - -def organize_WDKoester_atmospheres(path_to_dir): - """ - Construct cdbs-ready wdKoester WD atmospheres for each model. (from Koester 2010) - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/wdKoeseter - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.dk.dat.txt') - - for i in files: - data = Table.read(i, format='ascii') - - wave = data['col1'] # angstrom - flux = data['col2'] # erg/s/cm^2/A - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'/grid/wdKoester/'+i.replace('.txt', '.fits'), overwrite=True) - - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_WDKoester_catalog(path_to_dir): - """ - Create cdbs catalog.fits of wdKoester grid. - THIS IS STEP 2, after organize_WDKoester_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*dk.dat.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('.') - tmp2 = tmp[0].split('_') - temp = float(tmp2[0][2:]) # Kelvin - logg = float(tmp2[1]) / 100.0 # log(g) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_WDKoester(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin wdKoester models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: wdKoester_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing model; we will steal the header - tmp = cdbs_path+'/grid/wdKoester/da70000_800.dk.dat.fits' - wdkoester_hdu = fits.open(tmp) - header0 = wdkoester_hdu[0].header - wdkoester_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/wdKoester_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/wdKoester/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning wdKoester spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the wdKoester spectrum, rebin flux - sp = pysynphot.Icat('wdKoester', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - - diff --git a/spisea/atmospheres_LOCAL_58456.py b/spisea/atmospheres_LOCAL_58456.py deleted file mode 100644 index 0f4c90e5..00000000 --- a/spisea/atmospheres_LOCAL_58456.py +++ /dev/null @@ -1,2508 +0,0 @@ -import logging -import numpy as np -import pysynphot -import os -import glob -from astropy.io import fits -from astropy.table import Table, Column -import pysynphot -import time -import pdb -import warnings - -log = logging.getLogger('atmospheres') - -def get_atmosphere_bounds(model_dir, metallicity=0, temperature=20000, gravity=4): - """ - Given atmosphere model, get temperature and gravity bounds - """ - # Open catalog fits file and break out row indices - catalog = Table.read('{0}/grid/{1}/catalog.fits'.format(os.environ['PYSYN_CDBS'], model_dir)) - - teff_arr = [] - z_arr = [] - logg_arr = [] - for cur_row_index in range(len(catalog)): - index = catalog['INDEX'][cur_row_index] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Filter by metallicity. Will chose the closest metallicity to desired input - metal_list = np.unique(np.array(z_arr)) - metal_idx = np.argmin(np.abs(metal_list - metallicity)) - - z_filt = np.where(z_arr == metal_list[metal_idx]) - teff_arr = teff_arr[z_filt] - logg_arr = logg_arr[z_filt] - - # # Now find the closest atmosphere in parameter space to - # # the one we want. We'll find the match with the lowest - # # fractional difference - # teff_diff = (teff_arr - temperature) / temperature - # logg_diff = (logg_arr - gravity) / gravity - # - # diff_tot = abs(teff_diff) + abs(logg_diff) - # idx_f = np.argmin(diff_tot) - # - # temperature_new = teff_arr[idx_f] - # gravity_new = logg_arr[idx_f] - - # First check if temperature within bounds - temperature_new = temperature - if temperature > np.max(teff_arr): - temperature_new = np.max(teff_arr) - if temperature < np.min(teff_arr): - temperature_new = np.min(teff_arr) - - # If temperature within bounds, then check if metallicity within bounds - teff_diff = np.abs(teff_arr - temperature) - sorted_min_diffs = np.unique(teff_diff) - - ## Find two closest temperatures - teff_close_1 = teff_arr[np.where(teff_diff == sorted_min_diffs[0])[0][0]] - teff_close_2 = teff_arr[np.where(teff_diff == sorted_min_diffs[1])[0][0]] - - logg_arr_1 = logg_arr[np.where(teff_arr == teff_close_1)] - logg_arr_2 = logg_arr[np.where(teff_arr == teff_close_2)] - - ## Switch to most conservative bound of logg out of two closest temps - gravity_new = gravity - if gravity > np.min([np.max(logg_arr_1), np.max(logg_arr_2)]): - gravity_new = np.min([np.max(logg_arr_1), np.max(logg_arr_2)]) - if gravity < np.max([np.min(logg_arr_1), np.min(logg_arr_2)]): - gravity_new = np.max([np.min(logg_arr_1), np.min(logg_arr_2)]) - - # Print out changes, if any - if temperature_new != temperature: - teff_msg = 'Changing to T={0:6.0f} for T={1:6.0f} logg={2:4.2f}' - print( teff_msg.format(temperature_new, temperature, gravity)) - - if gravity_new != gravity: - logg_msg = 'Changing to logg={0:4.2f} for T={1:6.0f} logg={2:4.2f}' - print( logg_msg.format(gravity_new, temperature, gravity)) - - return (temperature_new, gravity_new) - -def get_kurucz_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmosphere from the Kurucz pysnphot grid - (`Kurucz 1993 `_). - - Grid Range: - - * Teff: 3000 - 50000 K - * gravity: 0 - 5 cgs - * metallicity: -5.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - Always false for this particular function - """ - try: - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('k93models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Kurucz 1993 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_castelli_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmospheres from the pysynphot ATLAS9 atlas - (`Castelli & Kurucz 2004 `_). - - Grid Range: - - * Teff: 3500 - 50000 K - * gravity: 0 - 5.0 cgs - * [M/H]: -2.5 - 0.2 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('ck04models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Castelli and Kurucz 2004 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_nextgen_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - try: - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('nextgen', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find NextGen atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_amesdusty_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - sp = pysynphot.Icat('AMESdusty', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find AMESdusty Allard+ 2000 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenix_atmosphere(metallicity=0, temperature=5000, gravity=4, - rebin=False): - """ - Return atmosphere from the pysynphot - `PHOENIX atlas `_. - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - try: - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIX BT-Settl (Allard+ 2011 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere(metallicity=0, temperature=24000, gravity=4.3, rebin=True): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - # Take care of atmospheres outside the catalog boundaries - logg_msg = 'Changing to logg={0:3.1f} for T={1:6.0f} logg={2:4.2f}' - if gravity > 4.3: - print( logg_msg.format(4.3, temperature, gravity)) - gravity = 4.3 - - if rebin: - sp = pysynphot.Icat('cmfgen_rot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_rot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere_closest(metallicity=0, temperature=24000, gravity=4.3, rebin=True, - verbose=False): - """ - For a given stellar atmosphere, get extract the closest possible match in - Teff/logg space. Note that this is different from the normal routine - which interpolates along the input grid to get final spectrum. We can't - do this here because the Fierro+15 atmosphere grid is so sparse - - rebin=True: pull from atmospheres at ck04model resolution. - - If verbose, print out the parameters of the match - """ - # Set up the proper root directory - if rebin == True: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot_rebin/' - else: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot/' - - # Read in catalog, extract atmosphere info - cat = Table.read('{0}/catalog.fits'.format(root_dir), format='fits') - teff_arr = [] - z_arr = [] - logg_arr = [] - for ii in range(len(cat)): - index = cat['INDEX'][ii] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Now find the closest atmosphere in parameter space to - # the one we want. We'll find the match with the lowest - # fractional difference - teff_diff = (teff_arr - temperature) / temperature - logg_diff = (logg_arr - gravity) / gravity - - diff_tot = abs(teff_diff) + abs(logg_diff) - idx_f = np.where(diff_tot == min(diff_tot))[0][0] - - # Extract the filename of the best-match model and read as - # pysynphot object - infile = cat[idx_f]['FILENAME'].split('.') - spec = Table.read('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Now, the CMFGEN atmospheres assume a distance of 1 kpc, while the the - # ATLAS models are in FLAM at the surface. So, we need to multiply the - # CMFGEN atmospheres by (1000/R)**2. in order to convert to FLAM on surface. - # We'll calculate radius from Teff and logL, which is given in the Table_*.txt file - t = Table.read('{0}/Table_rot.txt'.format(root_dir), format='ascii') - tmp = np.where(t['col1'] == infile[0]) - - lum = t['col3'][tmp] * (3.839*10**33) # cgs - sigma = 5.6704 * 10**-5 # cgs - teff = teff_arr[idx_f] # cgs - - radius = np.sqrt( lum / (4.0 * np.pi * teff**4. * sigma) ) # in cm - radius /= 3.08*10**18 # in pc - - - # Make the pysynphot spectrum - w = spec['Wavelength'] - f = spec['Flux'] * (1000 / radius)**2. - sp = pysynphot.ArraySpectrum(w,f) - - #sp = pysynphot.FileSpectrum('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Print out parameters of match, if desired - if verbose: - print('Teff match: Input: {0}, Output: {1}'.format(temperature, teff_arr[idx_f])) - print('logg match: Input: {0}, Output: {1}'.format(gravity, logg_arr[idx_f])) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=22500, gravity=3.98, rebin=True): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - if rebin: - sp = pysynphot.Icat('cmfgen_norot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_norot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=30000, gravity=4.14): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 30000) - gravity = log gravity (def = 4.14) - """ - sp = pysynphot.Icat('cmfgenF15_noRot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN non-rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenixv16_atmosphere(metallicity=0, temperature=4000, gravity=4, rebin=True): - """ - Return PHOENIX v16 atmospheres from - `Husser et al. 2013 `_. - - Models originally downloaded via `ftp `_. - Solar metallicity and [alpha/Fe] is used. - - Grid Range: - - * Teff: 2300 - 7000 K, steps of 100 K; 7000 - 12000 in steps of 200 K - * gravity: 0.0 - 6.0 cgs, steps of 0.5 - * [M/H]: -4.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - atm_model_name = 'phoenix_v16' - if rebin == True: - atm_model_name = 'phoenix_v16_rebin' - - - # Extract atmosphere. If that fails, then check bounds and try again - try: - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_model_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIXv16 (Husser+13) atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_2015_atmosphere(metallicity=0, temperature=2500, gravity=4, rebin=True): - """ - Return atmosphere from CIFIST2011_2015 grid - (`Allard et al. 2012 `_, - `Baraffe et al. 2015 `_ ) - - Grid originally downloaded from `website `_. - - Grid Range: - - * Teff: 1200 - 7000 K - * gravity: 2.5 - 5.5 cgs - * [M/H] = 0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - if rebin == True: - atm_name = 'BTSettl_2015_rebin' - else: - atm_name = 'BTSettl_2015' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - #print(dir(obj)) - - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl_2015 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_atmosphere(metallicity=0, temperature=2500, gravity=4.5, rebin=True): - """ - Return atmosphere from CIFIST2011 grid - (`Allard et al. 2012 `_) - - Grid originally downloaded `here `_ - - Notes - ------ - Grid Range: - - * [M/H] = -2.5, -2.0, -1.5, -1.0, -0.5, 0, 0.5 - - Teff and gravity ranges depend on metallicity: - - [M/H] = -2.5 - - * Teff: 2600 - 4600 K - * gravity: 4.5 - 5.5 - - [M/H] = -2.0 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.5 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.0 - - * Teff: 2600 - 7000 - * gravity: Teff < 3200 --> 4.5 - 5.5; Teff > 3200 --> 2.5 - 5.5 - - [M/H] = -0.5 - - * Teff: 1000 -7000 - * gravity: Teff < 3000 --> 4.5 - 5.5; Teff > 3000 --> 3.0 - 6.0 - - [M/H] = 0 - - * Teff: 750 - 7000 - * gravity: Teff < 2500 --> 3.5 - 5.5; Teff > 2500 --> 0 - 5.5 - - [M/H] = 0.5 - - * Teff: 1000 - 5000 - * gravity: 3.5 - 5.0 - - - Alpha enhancement: - - * [M/H]= -0.0, +0.5 no anhancement - * [M/H]= -0.5 with [alpha/H]=+0.2 - * [M/H]= -1.0, -1.5, -2.0, -2.5 with [alpha/H]=+0.4 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - **PRINT STATEMENTS TO DEBUG - **check get_atmosphere_bounds - **comment out try/except clause and check break - """ - if rebin == True: - atm_name = 'BTSettl_rebin' - else: - atm_name = 'BTSettl' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - -def get_Meisner2023_atmosphere(metallicity=0, temperature=1000, gravity=4.5, rebin=True): - """ - Return atmosphere from Meisner2023 grid - (`Meisner et al. 2023 `_) - - Grid originally downloaded `here `_ - - Grid range: - * Teff = 250 - 1200 K - * gravity: 2.5 - 5.5 cgs (in steps of 0.5) - * [M/H] = -1.0, -0.5, 0, +0, +0.3 - - """ - if rebin == True: - atm_name = 'Meisner2023_rebin' - else: - atm_name = 'Meisner2023' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Meisner2023 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_Phillips2020_atmosphere(metallicity=0, temperature=1000, gravity=4.5, rebin=True): - """ - Return atmosphere from Phillips et al., 2020 using ATMO model - (`Phillips et al. 2020 `_) - - Grid originally downloaded `here `_ - - Grid Range: - * Teff: 200 - 3000 K - * gravity: 2.5 - 5.5 cgs - * [M/H] = 0 - """ - if rebin == True: - atm_name = 'Phillips2020_rebin' - else: - atm_name = 'Phillips2020' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Phillips2020 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_wdKoester_atmosphere(metallicity=0, temperature=20000, gravity=7): - """ - Return white dwarf atmospheres from - `Koester et al. 2010 `_ - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - sp = pysynphot.Icat('wdKoester', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find WD Koester (Koester+ 2010 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_atlas_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of atlas ck04 model and phoenixV16. - - Only valid for temps between 5000 - 5500K, gravity from 0 = 5.0 - """ - try: - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('merged_atlas_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model - and phoenixV16. - - Only valid for temps between 3200 - 3800K, gravity from 2.5 - 5.5 - """ - try: - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('merged_BTSettl_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_meisner_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model - and Meisner2023. - - Only valid for temps between 1000 - 1200K, gravity from 3.5 - 5.5 - """ - try: - sp = pysynphot.Icat('merged_BTSettl_meisner', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity) = get_atmosphere_bounds('merged_BTSettl_meisner', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_BTSettl_meisner', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl-Meisner merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -#---------------------------------------------------------------------# -def get_merged_atmosphere(metallicity=0, temperature=20000, gravity=4.5, verbose=False, - rebin=True): - """ - Return a stellar atmosphere from a suite of different model grids, - depending on the input temperature, (all values in K). - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - - Notes - ----- - The underlying stellar model grid used changes as a function of - stellar temperature (in K): - - * T > 20,000: ATLAS - * 5500 <= T < 20,000: ATLAS - * 5000 <= T < 5500: ATLAS/PHOENIXv16 merge - * 3800 <= T < 5000: PHOENIXv16 - - For T < 3800, there is an additional gravity and metallicity - dependence: - - If T < 3800 and [M/H] = 0: - - * T < 3800, logg < 2.5: PHOENIX v16 - * 3200 <= T < 3800, logg > 2.5: BTSettl_CIFITS2011_2015/PHOENIXV16 merge - * 3200 < T <= 1200, logg > 2.5: BTSettl_CIFITS2011_2015 - * 1200 < T <= 1000, logg >= 3.5: BTSettl_CIFITS2011_2015/Meisner2023 merge - * 1000 < T <= 250, logg > 2.5: Meisner2023 - - Otherwise, if T < 3800 and [M/H] != 0: - - * T < 3800: PHOENIX v16 - - References: - - * ATLAS: ATLAS9 models (`Castelli & Kurucz 2004 `_) - * PHOENIXv16 (`Husser et al. 2013 `_) - * BTSettl_CIFITS2011_2015: Baraffee+15, Allard+ (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011_2015/SPECTRA/) - * Meisner2023: ATMO 1D models (`Meisner et al. 2023 `_) - - LTE WARNING: - - The ATLAS atmospheres are calculated with LTE, and so they - are less accurate when non-LTE conditions apply (e.g. T > 20,000 - K). Ultimately we'd like to add a non-LTE atmosphere grid for - the hottest stars in the future. - - HOW BOUNDARIES BETWEEN MODELS ARE TREATED: - - At the boundary between two models grids a temperature range is defined - where the resulting atmosphere is a weighted average between the two - grids. Near one boundary one model - is weighted more heavily, while at the other boundary the other - model is weighted more heavily. These are calculated in the - temperature ranges where we switch between model grids, to - ensure a smooth transition. - """ - # For T < 3800, atmosphere depends on metallicity + gravity. - # If solar metallicity, use BTSettl 2015 grid. Only solar metallicity is - # currently available here, so if non-solar metallicity, just stick with - # the Phoenix grid - if (temperature < 1000): - if verbose: - print( 'Meisner2023 atmosphere') - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 1200) & (temperature >= 1000): - if (gravity >= 3.5): - if verbose: - print( 'BTSettl/Meisner2023 merged atmosphere') - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - if (gravity < 3.5) & (gravity >=2.5): - if verbose: - print( 'Meisner2023 atmosphere') - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 3800) & (metallicity == 0): - # High gravity are in BTSettl regime - if (temperature <= 3200) & (gravity > 2.5): - if verbose: - print( 'BTSettl_2015 atmosphere') - return get_BTSettl_2015_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 3200) & (temperature < 3800) & (gravity > 2.5): - if verbose: - print( 'BTSettl/Phoenixv16 merged atmosphere') - return get_BTSettl_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - # Low gravity is PHOENIX regime - if gravity <= 2.5: - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 3800) & (metallicity != 0): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - # For T > 3800, no metallicity or gravity dependence - if (temperature >= 3800) & (temperature < 5000): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 5000) & (temperature < 5500): - if verbose: - print( 'ATLAS/Phoenix merged atmosphere') - return get_atlas_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if (temperature >= 5500) & (temperature < 20000): - if verbose: - print( 'ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if temperature >= 20000: - if verbose: - print( 'Still ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - #print('CMFGEN') - #return get_cmfgenRot_atmosphere_closest(metallicity=metallicity, - # temperature=temperature, - # gravity=gravity) - - - - -def get_wd_atmosphere(metallicity=0, temperature=20000, gravity=4, verbose=False): - """ - Return the white dwarf atmosphere from - `Koester et al. 2010 `_. - If desired parameters are - outside of grid, return a blackbody spectrum instead - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - if verbose: - print('wdKoester atmosphere') - - return get_wdKoester_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - except pysynphot.exceptions.ParameterOutOfBounds: - # Use a black-body atmosphere. - bbspec = get_bb_atmosphere(temperature=temperature, verbose=verbose) - return bbspec - - -def get_bd_atmosphere(metallicity=0, temperature=1000, gravity=4, verbose=False): - """ - Return the brown dwarf atmosphere from - `Meisner et al. 2023 `_. - If desired parameters are - outside of grid, return a blackbody spectrum instead - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - if verbose: - print('Meisner2023 atmosphere') - - return get_Meisner2023_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - except pysynphot.exceptions.ParameterOutOfBounds: - # Use a black-body atmosphere - bbspec = get_bb_atmosphere(temperature=temperature, verbose=verbose) - return bbspec - -def get_bb_atmosphere(metallicity=None, temperature=20_000, gravity=None, - verbose=False, rebin=None, - wave_min=500, wave_max=50_000, wave_num=20_000): - """ - Return a blackbody spectrum - - Parameters - ---------- - temperature: float, default=20_000 - The stellar temperature, in units of K - wave_min: float, default=500 - Sets the minimum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_max: float, default=50_000 - Sets the maximum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_num: int, default=20_000 - Sets the number of wavelength points in the wavelength range - Note: the wavelength range is evenly spaced in log space - """ - if ((metallicity is not None) or (gravity is not None) or - (rebin is not None)): - warnings.warn( - 'Only `temperature` keyword is used for black-body atmosphere' - ) - - if verbose: - print('Black-body atmosphere') - - # Modify pysynphot's default waveset to specified bounds - pysynphot.refs.set_default_waveset( - minwave=wave_min, maxwave=wave_max, num=wave_num - ) - - # Get black-body atmosphere for specified temperature from pysynphot - bbspec = pysynphot.spectrum.BlackBody(temperature) - - # pysynphot `BlackBody` generates spectrum in `photlam`, need in `flam` - bbspec.convert('flam') - - # `BlackBody` spectrum is normalized to solar radius star at 1 kiloparsec. - # Need to remove this normalization for SPISEA by multiplying bbspec - # by (1000 * 1 parsec / 1 Rsun)**2 = (1000 * 3.08e18 cm / 6.957e10 cm)**2 - bbspec *= (1000 * 3.086e18 / 6.957e10)**2 - - return bbspec - - -#--------------------------------------# -# Atmosphere formatting functions -#--------------------------------------# - -def download_CMFGEN_atmospheres(Table_rot, Table_norot): - """ - Downloads CMFGEN models from - https://sites.google.com/site/fluxesandcontinuum/home; - these contain continuum as well as lines. - - Table_rot, Table_norot are tables with the file prefixes - and model atmosphere parameters, taken by hand from the - Fierro+15 paper - - Website addresses are hardcoded - - Puts downloaded models in the current working directory. - """ - print( 'WARNING: THIS DOES NOT COMPLETELY WORK') - print( '**********************') - t_rot = Table.read(Table_rot, format='ascii') - t_norot = Table.read(Table_norot, format='ascii') - - tables = [t_rot, t_norot] - filenames = [t_rot['col1'], t_norot['col1']] - - # Hardcoded list of webiste addresses - web_base1 = 'https://sites.google.com/site/fluxesandcontinuum/home/' - web_base2 = 'https://sites.google.com/site/modelsobmassivestars/' - web = [web_base1+'009-solar-masses/',web_base1+'012-solar-masses/', - web_base1+'015-solar-masses/',web_base1+'020-solar-masses/', - web_base1+'025-solar-masses/',web_base2+'009-solar-masses-tracks/', - web_base2+'040-solar-masses/',web_base2+'060-solar-masses/', - web_base1+'085-solar-masses/',web_base1+'120-solar-masses/'] - # Array of masses that matches the website addresses - mass_arr = np.array([9.,12.,15.,20.,25.,32.,40.,60.,85.,120.]) - - # Loop through rotating and unrotating case. First loop is rot, second unrot - for i in range(2): - # Extract masses from filenames - masses = [] - for j in filenames[i]: - tmp = j.split('m') - mass = float(tmp[1][:-1]) - masses.append(mass) - - # Download the models webpage by webpage. A bit tricky because masses - # change slightly within a particular website. THIS IS WHAT FAILS - for j in range(len(web)): - if j == 0: - good = np.where( (masses <= mass_arr[j]) ) - else: - g = j - 1 - good = np.where( (masses <= mass_arr[j]) & - (masses > mass_arr[g]) ) - # Use wget command to pull down the files, and unzip them - for k in good[0]: - full = web[j]+'{1:s}.flx.zip'.format(mass_arr[j],filenames[i][k]) - os.system('wget ' + full) - os.system('unzip '+ filenames[i][k] + '.flx.zip') - - return - -def organize_CMFGEN_atmospheres(path_to_dir): - """ - Organize CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html) - into rot and noRot directories - - path_to_dir is from current working directory to directory - containing the downloaded models. Assumed that models - and tables describing parameters are in this directory. - - Tables describing parameters MUST be named Table_rot.txt, - Table_noRot.txt. Made by hand from Tables 3, 4 in Fierro+15. - These are located in same original directory as atmosphere files - - Will separate files into 2 subdirectories, one rotating and - the other non-rotating - - *Can't have any other files starting with "t" in model directory to start!* - """ - # First, record current working directory to return to later - start_dir = os.getcwd() - - # Enter atmosphere directory, collect rotating and non-rotating - # file names (assumed to all start with "t") - os.chdir(path_to_dir) - rot_models = glob.glob("t*r.flx*") - noRot_models = glob.glob("t*n.flx*") - - # Separate into different subdirectories - if os.path.exists('cmfgenF15_rot'): - pass - else: - os.mkdir('cmfgenF15_rot') - os.mkdir('cmfgenF15_noRot') - - for mod in rot_models: - cmd = 'mv {0:s} cmfgenF15_rot'.format(mod) - os.system(cmd) - - for mod in noRot_models: - cmd = 'mv {0:s} cmfgenF15_noRot'.format(mod) - os.system(cmd) - - # Also move Tables with model parameters into correct directory - os.system('mv Table_rot.txt cmfgenF15_rot') - os.system('mv Table_noRot.txt cmfgenF15_noRot') - - # Return to original directory - os.chdir(start_dir) - - return - -def make_CMFGEN_catalog(path_to_dir): - """ - Create cdbs catalog.fits of CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html). - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to directory - containing the rotating or non-rotating models (i.e. cmfgenF15_rot). Also, - needs to be a Table*.txt file which contains the parameters for all of the - original models, since params in filename are not precise enough - - Will create catalog.fits file in atmosphere directory with - description of each model - - *Can't have any other files starting with "t" in model directory to start!* - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere - # Note: can't rely on filename for this because not precise enough!! - - #---------OLD: GETTING PARAMS FROM FILENAME-------# - # Collect file names (assumed to all start with "t") - #files = glob.glob("t*") - #for name in files: - # tmp = name.split('l') - # temp = float(tmp[0][1:]) * 100.0 # In kelvin - - # lumtmp = tmp[1].split('_') - # lum = float(lumtmp[0][:-5]) * 1000.0 # In L_sun - - # mass = float(lumtmp[0][5:-1]) # In M_sun - - # Need to calculate log g from T and L (cgs) - # lum_sun = 3.846 * 10**33 # erg/s - # M_sun = 2 * 10**33 # g - # G_si = 6.67 * 10**(-8) # cgs - # sigma_si = 5.67 * 10**(-5) # cgs - - # g = (G_si * mass * M_sun * 4 * np.pi * sigma_si * temp**4) / \ - # (lum * lum_sun) - # logg = np.log10(g) - #---------------------------------------------------# - - # Read table with atmosphere params - table = glob.glob('Table_*') - t = Table.read(table[0], format = 'ascii') - names = t['col1'] - temps = t['col2'] - logg = t['col4'] - - # Create catalog.fits file - index_str = [] - name_str = [] - for i in range(len(names)): - index = '{0:5.0f},0.0,{1:3.2f}'.format(temps[i], logg[i]) - - #---NOTE: THE FOLLOWING DEPENDS ON FINAL LOCATION OF CATALOG FILE---# - #path = path_to_dir + '/' + names[i] - path = names[i] + '.fits[Flux]' - - index_str.append(index) - name_str.append(path) - - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits') - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def cdbs_cmfgen(path_to_dir, path_to_cdbs_dir): - """ - Code to put cmfgen models into cdbs format and adds proper unit keyword in - fits header. Save as fits file - - path_to_dir goes from current directory to cmfgen_rot or cmfgen_norot - directory with the *.flx models. Note that these files have already been - organized using organize_CMFGEN_atmospheres code. - - path_to_cdbs_dir goes from current directory to cdbs/grid/cmfgen_rot or - cmfgen_norot directory. Will copy new fits files to this directory. - This directory must already exist! - """ - # Save starting directory for later, move into path_to_dir directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('*.flx') - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - # Open file, extract useful info - t = Table.read(i, format='ascii') - wave = t['col1'] - flux = t['col2'] # Flux is already in erg/cm^2/s/A - - # Need to eliminate duplicate entries (pysynphot crashes) - unique = np.unique(wave, return_index=True) - wave = wave[unique[1]] - flux = flux[unique[1]] - - # Make fits table from individual columns. - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - #Adding unit keywords - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - prihdu = fits.PrimaryHDU() - - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i[:-4]+'.fits', overwrite=True) - - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Return to original directory, copy over new .fits files to cdbs directory - os.chdir(start_dir) - cmd = 'mv {0:s}/*.fits {1:s}'.format(path_to_dir, path_to_cdbs_dir) - os.system(cmd) - - return - -def rebin_cmfgen(cdbs_path, rot=True): - """ - Rebin cmfgen_rot and cmfgen_norot models to atlas ck04 resolution; - this makes spectrophotometry MUCH faster - - cdbs_path: path to cdbs directory - rot=True for rotating models (cmfgen_rot), False for non-rotating models - - makes new directory in cdbs/grid: cmfgen_rot_rebin or cmfgen_norot_rebin - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing cmfgen model; we will steal the header. - # Also define paths to new rebin directories - if rot == True: - tmp = cdbs_path+'/grid/cmfgen_rot/t0200l0008m009r.fits' - path = cdbs_path+'/grid/cmfgen_rot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_rot/' - else: - tmp = cdbs_path+'/grid/cmfgen_norot/t0200l0007m009n.fits' - path = cdbs_path+'/grid/cmfgen_norot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_norot/' - - cmfgen_hdu = fits.open(tmp) - header0 = cmfgen_hdu[0].header - # Create rebin directories if they don't already exist. Copy over - # catalog.fits file from original directory (will be the same) - if not os.path.exists(path): - os.mkdir(path) - cmd = 'cp {0:s}catalog.fits {1:s}'.format(orig_path, path) - os.system(cmd) - - # Read in the catalog.fits file - cat = fits.getdata(orig_path + 'catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - # First column in new files will be for [atlas] wavelength - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - - # For each catalog.fits entry, read the unbinned spectrum and rebin to - # the atlas resolution. Make a new fits file in rebin directory - count = 0 - for ff in range(len(files_all)): - count += 1 - # Extract the temp, Z, logg - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - grav = float(vals[2]) - - # Fetch the spectrum - if rot == True: - sp = pysynphot.Icat('cmfgen_rot', temp, metal, grav) - else: - sp = pysynphot.Icat('cmfgen_norot', temp, metal, grav) - - # Rebin - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - # Make the FITS file from the columns with header - cols = fits.ColDefs([c0,c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - # Write hdu to new directory with same filename - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(path+files_all[ff]) - - print( 'Finished file {0} of {1}'.format(count, len(files_all)) ) - return - - -def organize_PHOENIXv16_atmospheres(path_to_dir, met_str='m00'): - """ - Construct the Phoenix Husser+13 atmopsheres for each model. Combines the - fluxes from the *HiRES.fits files and the wavelengths of the - WAVE_PHONEIX-ACES-AGSS-COND-2011.fits file. - - path_to_dir is the path to the directory containing all of the downloaded - files - - met_str is the name of the current metallicity - - Creates new fits files for each atmosphere: phoenix_.fits, - which contains columns for the log g (column header = g#.#). Puts - atmospheres in new directory phoenixm00 - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the current metallicity subdirectory - sub_dir = '../phoenix{0}'.format(met_str) - if os.path.exists(sub_dir): - pass - else: - os.mkdir(sub_dir) - - # Extract wavelength array, make column for later - wavefile = fits.open('WAVE_PHOENIX-ACES-AGSS-COND-2011.fits') - wave = wavefile[0].data - wavefile.close() - wave_col = Column(wave, name = 'WAVELENGTH') - - # Create temp array for Husser+13 grid (given in paper) - temp_arr = np.arange(2300, 7001, 100) - temp_arr = np.append(temp_arr, np.arange(7000, 12001, 200)) - - print( 'Looping though all temps') - # For each temp, build file containing the flux for all gravities - i = 0 - for temp in temp_arr: - files = glob.glob('lte{0:05d}-*-HiRes.fits'.format(temp)) - files.sort() - # Start the table with the wavelength column - t = Table() - t.add_column(wave_col) - for f in files: - # Extract the logg out of filename - logg = f[9:13] - - # Extract fluxes from file - spectrum = fits.open(f) - flux = spectrum[0].data - spectrum.close() - - # Make Column object with fluxes, add to table - col = Column(flux, name = 'g{0:2.1f}'.format(float(logg))) - t.add_column(col) - - # Now, construct final fits file for the given temp - outname = 'phoenix{0}_{1:05d}.fits'.format(met_str, temp) - t.write('{0}/{1}'.format(sub_dir, outname), format = 'fits', overwrite = True) - - # Progress counter for user - i += 1 - print( 'Done {0:d} of {1:d}'.format(i, len(temp_arr))) - - # Return to original directory - os.chdir(start_dir) - return - -def make_PHOENIXv16_catalog(path_to_dir, met_str='m00'): - """ - Makes catalog.fits file for Husser+13 phoenix models. Assumes that - organize_PHOENIXv16_atmospheres has been run already, and that the models lie - in subdirectory phoenix[met_str]. - - path_to_directory is the path to the directory with the reformatted - models (i.e. the output from construct_atmospheres, phoenix[met_str]) - - Puts catalog.fits file in directory the user starts in - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Extract metallicity from metallicity string - met = float(met_str[1]) + (float(met_str[2]) * 0.1) - if 'm' in met_str: - met *= -1. - - # Collect the filenames. Each is a unique temp with many different log g's - files = glob.glob('phoenix*.fits') - files.sort() - - # Create the catalog.fits file, row by row - index_arr = [] - filename_arr = [] - for i in files: - # Get log g values from the column header the file - t = Table.read(i, format='fits') - keys = t.keys() - logg_vals = keys[1:] - - # Extract temp from filename - name = i.split('_') - temp = float(name[1][:-5]) - for j in logg_vals: - logg = float(j[1:]) - index = '{0:5.0f},{1:2.1f},{2:2.1f}'.format(temp, met, logg) - filename = path_to_dir + i + '[' + j + ']' - # Add row to table - index_arr.append(index) - filename_arr.append(filename) - - catalog = Table([index_arr, filename_arr], names=('INDEX', 'FILENAME')) - - # Return to starting directory, write catalog - os.chdir(start_dir) - - if os.path.exists('catalog.fits'): - from astropy.table import vstack - - prev_catalog = Table.read('catalog.fits', format='fits') - joined_catalog = vstack([prev_catalog, catalog]) - - joined_catalog.write('catalog.fits', format='fits', overwrite=True) - else: - catalog.write('catalog.fits', format='fits', overwrite=True) - - return - -def cdbs_PHOENIXv16(path_to_cdbs_dir): - """ - Put the PHOENIXv16 (Husser+13) fits files into cdbs format. This primarily - consists of adjusting the flux units from [erg/s/cm^2/cm] to [erg/s/cm^2/A] - and adding the appropriate keywords to the fits header. - - path_to_cdbs_dir goes from current working directory to phoenix[met] directory - in cdbs/grids/phoenix_v16. Note that these files have already been organized - using organize_PHOENIXv16_atmospheres code. - - Overwrites original files in directory - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_cdbs_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('phoenix*.fits') - - ## Need to sort filenames; glob doesn't always give them in order - files.sort() - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - - # Read in current FITS table - cur_table = Table.read(i, format='fits') - - cur_table.columns[0].name = 'Wavelength' - - num_cols = len(cur_table.colnames) - - # Multiplying each flux column by 10^-8 for conversion - for cur_col_index in range(1, num_cols, 1): - cur_col_name = cur_table.colnames[cur_col_index] - cur_table[cur_col_name] = cur_table[cur_col_name] * 10.**-8 - - - # Construct new FITS file based on old one - hdu = fits.open(i) - header_0 = hdu[0].header - header_1 = hdu[1].header - sci = hdu[1].data - - tbhdu = fits.table_to_hdu(cur_table) - - # Copying over the older headers, adding unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - tbhdu.header['TUNIT3'] = 'FLAM' - tbhdu.header['TUNIT4'] = 'FLAM' - tbhdu.header['TUNIT5'] = 'FLAM' - tbhdu.header['TUNIT6'] = 'FLAM' - tbhdu.header['TUNIT7'] = 'FLAM' - tbhdu.header['TUNIT8'] = 'FLAM' - tbhdu.header['TUNIT9'] = 'FLAM' - tbhdu.header['TUNIT10'] = 'FLAM' - tbhdu.header['TUNIT11'] = 'FLAM' - tbhdu.header['TUNIT12'] = 'FLAM' - tbhdu.header['TUNIT13'] = 'FLAM' - tbhdu.header['TUNIT14'] = 'FLAM' - - # Construct and write out final FITS file - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i, overwrite=True) - - hdu.close() - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Change back to starting directory - os.chdir(start_dir) - - return - -def rebin_phoenixV16(cdbs_path): - """ - Rebin phoenixV16 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: phoenix_v16_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - ## (This assumes that at least 'm00' metallicity exists) - tmp = '{0}/grid/phoenix_v16/phoenix{1}/phoenix{1}_02400.fits'.format(cdbs_path, 'm00') - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/phoenix_v16_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/phoenix_v16/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - temp_arr = np.zeros(len(files_all), dtype=float) - logg_arr = np.zeros(len(files_all), dtype=float) - metal_arr = np.zeros(len(files_all), dtype=float) - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - - temp_arr[ff] = float(vals[0]) - metal_arr[ff] = float(vals[1]) - logg_arr[ff] = float(vals[2]) - - - metal_uniq = np.unique(metal_arr) - temp_uniq = np.unique(temp_arr) - - for mm in range(len(metal_uniq)): - metal = metal_uniq[mm] # metallicity - - # Construct str for metallicity (for appropriate directory name) - met_str = str(int(np.abs(metal))) + str(int((metal % 1.0)*10)) - if metal > 0: - met_str = 'p' + met_str - else: - met_str = 'm' + met_str - - # Make directory for current metallicity if it does not exist yet - if not os.path.exists(path + 'phoenix' + met_str): - os.mkdir(path + 'phoenix' + met_str) - - for tt in range(len(temp_uniq)): - temp = temp_uniq[tt] # temperature - - # Pick out the list of gravities for this T, Z combo - idx = np.where((metal_arr == metal) & (temp_arr == temp))[0] - logg_exist = logg_arr[idx] - - # All gravities will go in one file. Here is the output - # file name. - outfile = path + files_all[idx[0]].split('[')[0] - - ## If the rebinned file already exists, continue - if os.path.exists(outfile): - continue - - # Build a columns array. One column for each gravity. - cols_arr = [] - - # Make the wavelength column, which is first in the cols array. - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - cols_arr.append(c0) - - for gg in range(len(logg_exist)): - grav = logg_exist[gg] # gravity - - # Fetch the spectrum - sp = pysynphot.Icat('phoenix_v16', temp, metal, grav) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Store the spectrum - name = 'g{0:3.1f}'.format(grav) - col = fits.Column(name=name, format='E', array=flux_rebin) - cols_arr.append(col) - - - # Make the FITS file from the columns with header. - cols = fits.ColDefs(cols_arr) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - for gg in range(len(logg_exist)): - tbhdu.header['TUNIT{0:d}'.format(gg+2)] = 'FLAM' - - # Write hdu - finalhdu = fits.HDUList([prihdu, tbhdu]) - # don't have overwrite to protect original files. - finalhdu.writeto(outfile) - - print( 'Finished file ' + outfile + ' with gravities: ', logg_exist) - - - return - - -def rebin_spec(wave, specin, wavnew): - """ - Helper routine to rebin spectra. TAKEN FROM ASTROBETTER BLOG FROM JESSICA: - http://www.astrobetter.com/blog/2013/08/12/ - python-tip-re-sampling-spectra-with-pysynphot/ - """ - spec = pysynphot.spectrum.ArraySourceSpectrum(wave=wave, flux=specin) - f = np.ones(len(wave)) - filt = pysynphot.spectrum.ArraySpectralElement(wave, f, waveunits='angstrom') - obs = pysynphot.observation.Observation(spec, filt, binset=wavnew, force='taper') - - return obs.binflux - -def organize_BTSettl_2015_atmospheres(path_to_dir): - """ - Construct cdbs-ready BTSettl_CIFITS_2011_2015 atmospheres for each model. - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/grid/BTSettl_2015 - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the BTSettl subdirectory - if not os.path.exists('BTSettl_2015'): - os.mkdir('BTSettl_2015') - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.spec.fits') - - for i in files: - hdu = fits.open(i) - spec = hdu[1].data - header_0 = hdu[0].header - header_1 = hdu[1].header - - wave = spec.field(0) - flux = spec.field(1) - - # Get units right: convert wave from microns to Angstroms, - # flux from W /m^2/ micron to erg/s/cm^2/A - wave_new = wave * 10**4 - flux_new = flux * 10**(-1) - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave_new) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'grid/BTSettl_2015/'+i, overwrite=True) - - hdu.close() - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_BTSettl_2015_catalog(path_to_dir): - """ - Create cdbs catalog.fits of BTSettl_CIFITS2011_2015 grid. - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*spec.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('-') - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl_2015(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin BTSettle_CIFITS2011_2015 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: BTSettl_2015_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - tmp = cdbs_path+'/grid/phoenix_v16/phoenixm00/phoenixm00_02400.fits' - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - phoenix_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/BTSettl_2015_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/BTSettl_2015/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning BTSettl spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - sp = pysynphot.Icat('BTSettl_2015', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - -def make_wavelength_unique(files, dirname): - """ - Helper function to go through each BTSettl spectrum and ensure that - each wavelength point is unique. This is required for rebinning to work. - - - files: list of files to run this analysis on - """ - # Loop through each file, find fix repeated wavelength entries if necessary - for i in files: - t = Table.read('{0}/{1}'.format(dirname,i), format='fits') - test = np.unique(t['Wavelength'], return_index=True) - - if len(t) != len(test[0]): - t = t[test[1]] - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - # Also make sure wavelength is monotonic. If it is not, then it is - # a sign that the wavelengths are out of order - diff = np.diff(t['Wavelength']) - bad = np.where(diff < 0) - if len(bad[0]) > 0: - t.sort('Wavelength') - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - print('Done {0}'.format(i)) - - return - -def organize_BTSettl_atmospheres(): - """ - Construct cdbs-ready atmospheres for the BTSettl grid (CIFITS2011). - The code expects tp be run in cdbs/grid/BTSettl, and expects that the - individual model files have been downloaded from online - (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011/SPECTRA/) - and processed into python-readable ascii files. - """ - orig_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btm10', 'btm05', 'btp00', 'btp05'] - - - # Go through each directory, turning each spectrum into a cdbs-ready file. - # Will convert flux into Ergs/sec/cm**2/A (FLAM) units and save as a fits file, - # for faster access later - for ii in dirs: - print('Starting {0}'.format(ii)) - os.chdir(ii) - - files = glob.glob('*.txt') - count=0 - for jj in files: - t = Table.read(jj, format='ascii') - # First, trim the wavelengths to a more reasonable wavelength range - good = np.where( (t['col1'] > 1000) & (t['col1'] < 70000) ) - t = t[good] - - # Convert flux units to Flam (Ergs/sec/cm**2/A) - flux_new = 10**(t['col2'] - 8.0) - - # Save the file as a fits file - c0 = fits.Column(name='Wavelength', format='D', array=t['col1']) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Add unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto('{0}.fits'.format(jj[:-4]), overwrite=True) - hdu_new.close() - count += 1 - print('Done {0} of {1}'.format(count, len(files))) - - # Now, clean up all the files made when unzipping the spectra - cmd1 = 'rm *.bz2' - cmd2 = 'rm *.tmp' - #cmd3 = 'rm *.txt' - os.system(cmd1) - os.system(cmd2) - #os.system(cmd3) - print('==============================') - print('Done {0}'.format(ii)) - print('==============================') - - # Go back to original directory, move to next metallicity directory - os.chdir(orig_dir) - - return - -def make_BTSettl_catalog(): - """ - Create cdbs catalog.fits of BTSettl grid. - THIS IS STEP 2, after organize_BTSettl_atmospheres has - been run. - - Code expects to be run in cdbs/grid/BTSettl - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btp05'] - - # Construct the catalog.fits file input. The input consists of - # and index string that specifies the stellar paramters, and a - # name string that points to the file - # Loop over all the metallicity directories to construct these inputs - index_str = [] - name_str = [] - for ii in dirs: - os.chdir(ii) - files = glob.glob('*.fits') - - # Construct the metallicity val - if 'm' in ii: - metal_flag = -1 * float(ii[3:])*0.1 - else: - metal_flag = float(ii[3:])*0.1 - - # Now collect the info from the files - for jj in files: - tmp = jj.split('-') - - if metal_flag >= 0: - temp = float(tmp[0].split('+')[0][3:]) * 100.0 # In kelvin - try: - logg = float(tmp[1]) - except: - logg = float(tmp[1].split('+')[0]) - else: - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0},{1},{2:3.2f}'.format(int(temp), metal_flag, logg)) - name_str.append('{0}/{1}[Flux]'.format(ii, jj)) - - # Go back to original directory to move to next metallicity - print('Done {0}'.format(ii)) - os.chdir(start_dir) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl(make_unique=False): - """ - Rebin BTSettle models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory: BTSettl_rebin - - Code expects to be run in cdbs/grid directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Create cdbs/grid directory for rebinned models - path = 'BTSettl_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata('BTSettl/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - #==============================# - #tmp = [] - #for ii in files_all: - # if ii.startswith('btp00'): - # tmp.append(ii) - #files_all = tmp - #=============================# - - print( 'Rebinning BTSettl spectra') - if make_unique: - print('Making unique') - make_wavelength_unique(files_all, 'BTSettl') - print('Done') - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - try: - sp = pysynphot.Icat('BTSettl', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - except: - pdb.set_trace() - orig_file = '{0}/{1}'.format('BTSettl/', files_all[ff].split('[')[0]) - outfile = path + files_all[ff].split('[')[0] - cmd = 'cp {0} {1}'.format(orig_file, outfile) - os.system(cmd) - - print('Done {0} of {1}'.format(ff, len(files_all))) - - return - -def organize_all_Meisner2023_atmospheres(): - """ - Construct cdbs-ready atmospheres for the Meisner2023 grid. - The code expects tp be run in cdbs/grid/Meisner2023, and expects that the - individual model files have been downloaded from online - and processed into python-readable ascii files. - """ - orig_dir = os.getcwd() - dirs = ['mm10', 'mm05', 'mp00', 'mp03'] - - # Go through each directory, turning each spectrum into a cdbs-ready file. - # Save as a fits file, for faster access later - for ii in dirs: - print('Starting {0}'.format(ii)) - os.chdir(ii) - - files = glob.glob('*.fits') - count=0 - for jj in files: - # Open each .fits file and read the data - with fits.open(jj) as hdul: - data = hdul[1].data - wavelength = data['Wavelength'] - flux = data['Flux'] - - # Make flux independent of R&D - flux_new = flux / 5e-20 - - # Create new columns with desired format - c0 = fits.Column(name='Wavelength', format='D', array=wavelength) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Add unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write the new fits table in the cdbs directory - output_filename = '{0}.fits'.format(jj[:-5]) # Removing the original .fits extension - hdu_new.writeto(output_filename, overwrite=True) - hdu_new.close() - count += 1 - print('Done {0} of {1}'.format(count, len(files))) - - # Go back to original directory, move to next metallicity directory - os.chdir(orig_dir) - - return - -def make_Meisner2023_catalog(): - """ - Create cdbs catalog.fits of Meisner2023 grid. - THIS IS STEP 2, after organize_Meisner2023_atmospheres has - been run. - - Code expects to be run in cdbs/grid/Meisner2023 - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - dirs = ['mm10', 'mm05', 'mp00', 'mp03'] - - # Construct the catalog.fits file input. The input consists of - # and index string that specifies the stellar paramters, and a - # name string that points to the file - # Loop over all the metallicity directories to construct these inputs - index_str = [] - name_str = [] - for ii in dirs: - os.chdir(ii) - files = glob.glob('spec_jwst_*.fits') - - for jj in files: - # Parse temperature, log(g), and metallicity from filename - temp_str = jj.split('_')[2] - logg_str = jj.split('_')[3] - metal_str = jj.split('_')[4] - - # Extract temperature, surface gravity, and metallicity - temp = float(temp_str[1:]) # Temperature in Kelvin - logg = float(logg_str[1:]) # Surface gravity log(g) - - # Build metallicity value - if metal_str.startswith('m'): - metallicity = -1 * float(metal_str[1:]) - else: - metallicity = float(metal_str[1:]) - - # Construct index and filename strings - index_str.append('{0},{1},{2:3.2f}'.format(int(temp), metallicity, logg)) - name_str.append('{0}/{1}[Flux]'.format(ii, jj)) - - print('Processed directory:', ii) - os.chdir(start_dir) - - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_Meisner2023(make_unique=False): - """ - Rebin Meisner2023 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory: Meisner2023_rebin - - Code expects to be run in cdbs/grid directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Create a directory for rebinned Meisner2023 models - rebin_path = 'Meisner2023_rebin/' - if not os.path.exists(rebin_path): - os.mkdir(rebin_path) - - # Load the catalog.fits file and extract all spectra file paths - cat = Table.read('Meisner2023/catalog.fits') - files_all = [cat[ii]['FILENAME'].split('[')[0] for ii in range(len(cat))] - - print('Rebinning Meisner2023 spectra') - if make_unique: - print('Making unique') - make_wavelength_unique(files_all, 'Meisner2023') - print('Done') - - for ff, file in enumerate(files_all): - vals = cat[ff]['INDEX'].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the Meisner2023 spectrum and rebin its flux - try: - sp = pysynphot.Icat('Meisner2023', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Create the output FITS file - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - # Write the new rebinned file in the Meisner2023_rebin directory - outfile = os.path.join(rebin_path, os.path.basename(file)) - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - except Exception as e: - print(f"Error processing {file}: {e}") - orig_file = os.path.join('Meisner2023', file) - outfile = os.path.join(rebin_path, os.path.basename(file)) - os.system(f'cp {orig_file} {outfile}') - - print('Done {0} of {1}'.format(ff + 1, len(files_all))) - - return - - - -def organize_WDKoester_atmospheres(path_to_dir): - """ - Construct cdbs-ready wdKoester WD atmospheres for each model. (from Koester 2010) - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/wdKoeseter - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.dk.dat.txt') - - for i in files: - data = Table.read(i, format='ascii') - - wave = data['col1'] # angstrom - flux = data['col2'] # erg/s/cm^2/A - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'/grid/wdKoester/'+i.replace('.txt', '.fits'), overwrite=True) - - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_WDKoester_catalog(path_to_dir): - """ - Create cdbs catalog.fits of wdKoester grid. - THIS IS STEP 2, after organize_WDKoester_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*dk.dat.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('.') - tmp2 = tmp[0].split('_') - temp = float(tmp2[0][2:]) # Kelvin - logg = float(tmp2[1]) / 100.0 # log(g) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_WDKoester(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin wdKoester models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: wdKoester_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing model; we will steal the header - tmp = cdbs_path+'/grid/wdKoester/da70000_800.dk.dat.fits' - wdkoester_hdu = fits.open(tmp) - header0 = wdkoester_hdu[0].header - wdkoester_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/wdKoester_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/wdKoester/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning wdKoester spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the wdKoester spectrum, rebin flux - sp = pysynphot.Icat('wdKoester', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - - diff --git a/spisea/atmospheres_REMOTE_58456.py b/spisea/atmospheres_REMOTE_58456.py deleted file mode 100644 index 9867db81..00000000 --- a/spisea/atmospheres_REMOTE_58456.py +++ /dev/null @@ -1,2172 +0,0 @@ -import logging -import numpy as np -import pysynphot -import os -import glob -from astropy.io import fits -from astropy.table import Table, Column -import pysynphot -import time -import pdb -import warnings - -log = logging.getLogger('atmospheres') - -def get_atmosphere_bounds(model_dir, metallicity=0, temperature=20000, gravity=4, verbose=False): - """ - Given atmosphere model, get temperature and gravity bounds - """ - # Open catalog fits file and break out row indices - catalog = Table.read('{0}/grid/{1}/catalog.fits'.format(os.environ['PYSYN_CDBS'], model_dir)) - - teff_arr = [] - z_arr = [] - logg_arr = [] - for cur_row_index in range(len(catalog)): - index = catalog['INDEX'][cur_row_index] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Filter by metallicity. Will chose the closest metallicity to desired input - metal_list = np.unique(np.array(z_arr)) - metal_idx = np.argmin(np.abs(metal_list - metallicity)) - metallicity_new = metal_list[metal_idx] - - z_filt = np.where(z_arr == metal_list[metal_idx]) - teff_arr = teff_arr[z_filt] - logg_arr = logg_arr[z_filt] - - # # Now find the closest atmosphere in parameter space to - # # the one we want. We'll find the match with the lowest - # # fractional difference - # teff_diff = (teff_arr - temperature) / temperature - # logg_diff = (logg_arr - gravity) / gravity - # - # diff_tot = abs(teff_diff) + abs(logg_diff) - # idx_f = np.argmin(diff_tot) - # - # temperature_new = teff_arr[idx_f] - # gravity_new = logg_arr[idx_f] - - # First check if temperature within bounds - temperature_new = temperature - if temperature > np.max(teff_arr): - temperature_new = np.max(teff_arr) - if temperature < np.min(teff_arr): - temperature_new = np.min(teff_arr) - - # If temperature within bounds, then check if metallicity within bounds - teff_diff = np.abs(teff_arr - temperature) - sorted_min_diffs = np.unique(teff_diff) - - ## Find two closest temperatures - teff_close_1 = teff_arr[np.where(teff_diff == sorted_min_diffs[0])[0][0]] - teff_close_2 = teff_arr[np.where(teff_diff == sorted_min_diffs[1])[0][0]] - - logg_arr_1 = logg_arr[np.where(teff_arr == teff_close_1)] - logg_arr_2 = logg_arr[np.where(teff_arr == teff_close_2)] - - ## Switch to most conservative bound of logg out of two closest temps - gravity_new = gravity - if gravity > np.min([np.max(logg_arr_1), np.max(logg_arr_2)]): - gravity_new = np.min([np.max(logg_arr_1), np.max(logg_arr_2)]) - if gravity < np.max([np.min(logg_arr_1), np.min(logg_arr_2)]): - gravity_new = np.max([np.min(logg_arr_1), np.min(logg_arr_2)]) - - if verbose: - # Print out changes, if any - if temperature_new != temperature: - teff_msg = 'Changing to T={0:6.0f} for met={1:4.2f} T={2:6.0f} logg={3:4.2f}' - print( teff_msg.format(temperature_new, metallicity, temperature, gravity)) - - if gravity_new != gravity: - logg_msg = 'Changing to logg={0:4.2f} for met={1:4.2f} T={2:6.0f} logg={3:4.2f}' - print( logg_msg.format(gravity_new, metallicity, temperature, gravity)) - - if metallicity_new != metallicity: - logg_msg = 'Changing to met={0:4.2f} for met={1:4.2f} T={2:6.0f} logg={3:4.2f}' - print( logg_msg.format(metallicity_new, metallicity, temperature, gravity)) - - return (temperature_new, gravity_new, metallicity_new) - -def get_kurucz_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmosphere from the Kurucz pysnphot grid - (`Kurucz 1993 `_). - - Grid Range: - - * Teff: 3000 - 50000 K - * gravity: 0 - 5 cgs - * metallicity: -5.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - Always false for this particular function - """ - try: - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('k93models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('k93models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Kurucz 1993 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_castelli_atmosphere(metallicity=0, temperature=20000, gravity=4, rebin=False): - """ - Return atmospheres from the pysynphot ATLAS9 atlas - (`Castelli & Kurucz 2004 `_). - - Grid Range: - - * Teff: 3500 - 50000 K - * gravity: 0 - 5.0 cgs - * [M/H]: -2.5 - 0.2 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('ck04models', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('ck04models', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find Castelli and Kurucz 2004 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_nextgen_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - try: - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('nextgen', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('nextgen', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find NextGen atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_amesdusty_atmosphere(metallicity=0, temperature=5000, gravity=4, rebin=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 5000) - gravity = log gravity (def = 4.0) - """ - sp = pysynphot.Icat('AMESdusty', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find AMESdusty Allard+ 2000 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenix_atmosphere(metallicity=0, temperature=5000, gravity=4, - rebin=False): - """ - Return atmosphere from the pysynphot - `PHOENIX atlas `_. - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - try: - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIX BT-Settl (Allard+ 2011 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere(metallicity=0, temperature=24000, gravity=4.3, rebin=True, verbose=False): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - # Take care of atmospheres outside the catalog boundaries - logg_msg = 'Changing to logg={0:3.1f} for T={1:6.0f} logg={2:4.2f}' - if gravity > 4.3: - if verbose: - print( logg_msg.format(4.3, temperature, gravity)) - gravity = 4.3 - - if rebin: - sp = pysynphot.Icat('cmfgen_rot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_rot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenRot_atmosphere_closest(metallicity=0, temperature=24000, gravity=4.3, rebin=True, - verbose=False): - """ - For a given stellar atmosphere, get extract the closest possible match in - Teff/logg space. Note that this is different from the normal routine - which interpolates along the input grid to get final spectrum. We can't - do this here because the Fierro+15 atmosphere grid is so sparse - - rebin=True: pull from atmospheres at ck04model resolution. - - If verbose, print out the parameters of the match - """ - # Set up the proper root directory - if rebin == True: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot_rebin/' - else: - root_dir = os.environ['PYSYN_CDBS'] + '/cmfgen_rot/' - - # Read in catalog, extract atmosphere info - cat = Table.read('{0}/catalog.fits'.format(root_dir), format='fits') - teff_arr = [] - z_arr = [] - logg_arr = [] - for ii in range(len(cat)): - index = cat['INDEX'][ii] - tmp = index.split(',') - teff_arr.append(float(tmp[0])) - z_arr.append(float(tmp[1])) - logg_arr.append(float(tmp[2])) - teff_arr = np.array(teff_arr) - z_arr = np.array(z_arr) - logg_arr = np.array(logg_arr) - - # Now find the closest atmosphere in parameter space to - # the one we want. We'll find the match with the lowest - # fractional difference - teff_diff = (teff_arr - temperature) / temperature - logg_diff = (logg_arr - gravity) / gravity - - diff_tot = abs(teff_diff) + abs(logg_diff) - idx_f = np.where(diff_tot == min(diff_tot))[0][0] - - # Extract the filename of the best-match model and read as - # pysynphot object - infile = cat[idx_f]['FILENAME'].split('.') - spec = Table.read('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Now, the CMFGEN atmospheres assume a distance of 1 kpc, while the the - # ATLAS models are in FLAM at the surface. So, we need to multiply the - # CMFGEN atmospheres by (1000/R)**2. in order to convert to FLAM on surface. - # We'll calculate radius from Teff and logL, which is given in the Table_*.txt file - t = Table.read('{0}/Table_rot.txt'.format(root_dir), format='ascii') - tmp = np.where(t['col1'] == infile[0]) - - lum = t['col3'][tmp] * (3.839*10**33) # cgs - sigma = 5.6704 * 10**-5 # cgs - teff = teff_arr[idx_f] # cgs - - radius = np.sqrt( lum / (4.0 * np.pi * teff**4. * sigma) ) # in cm - radius /= 3.08*10**18 # in pc - - - # Make the pysynphot spectrum - w = spec['Wavelength'] - f = spec['Flux'] * (1000 / radius)**2. - sp = pysynphot.ArraySpectrum(w,f) - - #sp = pysynphot.FileSpectrum('{0}/{1}.fits'.format(root_dir, infile[0])) - - # Print out parameters of match, if desired - if verbose: - print('Teff match: Input: {0}, Output: {1}'.format(temperature, teff_arr[idx_f])) - print('logg match: Input: {0}, Output: {1}'.format(gravity, logg_arr[idx_f])) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=22500, gravity=3.98, rebin=True): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 24000) - gravity = log gravity (def = 4.3) - - rebin=True: pull from atmospheres at ck04model resolution. - """ - if rebin: - sp = pysynphot.Icat('cmfgen_norot_rebin', temperature, metallicity, gravity) - else: - sp = pysynphot.Icat('cmfgen_norot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_cmfgenNoRot_atmosphere(metallicity=0, temperature=30000, gravity=4.14): - """ - metallicity = [M/H] (def = 0) - temperature = Kelvin (def = 30000) - gravity = log gravity (def = 4.14) - """ - sp = pysynphot.Icat('cmfgenF15_noRot', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find CMFGEN non-rotating atmosphere model (Fierro+15) for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_phoenixv16_atmosphere(metallicity=0, temperature=4000, gravity=4, rebin=True): - """ - Return PHOENIX v16 atmospheres from - `Husser et al. 2013 `_. - - Models originally downloaded via `ftp `_. - Solar metallicity and [alpha/Fe] is used. - - Grid Range: - - * Teff: 2300 - 7000 K, steps of 100 K; 7000 - 12000 in steps of 200 K - * gravity: 0.0 - 6.0 cgs, steps of 0.5 - * [M/H]: -4.0 - 1.0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - """ - atm_model_name = 'phoenix_v16' - if rebin == True: - atm_model_name = 'phoenix_v16_rebin' - - - # Extract atmosphere. If that fails, then check bounds and try again - try: - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds(atm_model_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_model_name, temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find PHOENIXv16 (Husser+13) atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_2015_atmosphere(metallicity=0, temperature=2500, gravity=4, rebin=True): - """ - Return atmosphere from CIFIST2011_2015 grid - (`Allard et al. 2012 `_, - `Baraffe et al. 2015 `_ ) - - Grid originally downloaded from `website `_. - - Grid Range: - - * Teff: 1200 - 7000 K - * gravity: 2.5 - 5.5 cgs - * [M/H] = 0 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - if rebin == True: - atm_name = 'BTSettl_2015_rebin' - else: - atm_name = 'BTSettl_2015' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl_2015 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_atmosphere(metallicity=0, temperature=2500, gravity=4.5, rebin=True): - """ - Return atmosphere from CIFIST2011 grid - (`Allard et al. 2012 `_) - - Grid originally downloaded `here `_ - - Notes - ------ - Grid Range: - - * [M/H] = -2.5, -2.0, -1.5, -1.0, -0.5, 0, 0.5 - - Teff and gravity ranges depend on metallicity: - - [M/H] = -2.5 - - * Teff: 2600 - 4600 K - * gravity: 4.5 - 5.5 - - [M/H] = -2.0 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.5 - - * Teff: 2600 - 7000 - * gravity: 4.5 - 5.5 - - [M/H] = -1.0 - - * Teff: 2600 - 7000 - * gravity: Teff < 3200 --> 4.5 - 5.5; Teff > 3200 --> 2.5 - 5.5 - - [M/H] = -0.5 - - * Teff: 1000 -7000 - * gravity: Teff < 3000 --> 4.5 - 5.5; Teff > 3000 --> 3.0 - 6.0 - - [M/H] = 0 - - * Teff: 750 - 7000 - * gravity: Teff < 2500 --> 3.5 - 5.5; Teff > 2500 --> 0 - 5.5 - - [M/H] = 0.5 - - * Teff: 1000 - 5000 - * gravity: 3.5 - 5.0 - - - Alpha enhancement: - - * [M/H]= -0.0, +0.5 no anhancement - * [M/H]= -0.5 with [alpha/H]=+0.2 - * [M/H]= -1.0, -1.5, -2.0, -2.5 with [alpha/H]=+0.4 - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - if rebin == True: - atm_name = 'BTSettl_rebin' - else: - atm_name = 'BTSettl' - - try: - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds(atm_name, - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat(atm_name, temperature, metallicity, gravity) - - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find BTSettl_2015 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_wdKoester_atmosphere(metallicity=0, temperature=20000, gravity=7): - """ - Return white dwarf atmospheres from - `Koester et al. 2010 `_ - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - """ - sp = pysynphot.Icat('wdKoester', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find WD Koester (Koester+ 2010 atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_atlas_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of atlas ck04 model and phoenixV16. - - Only valid for temps between 5000 - 5500K, gravity from 0 = 5.0 - """ - try: - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('merged_atlas_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_atlas_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -def get_BTSettl_phoenix_atmosphere(metallicity=0, temperature=5250, gravity=4): - """ - Return atmosphere that is a linear merge of BTSettl_CITFITS2011_2015 model - and phoenixV16. - - Only valid for temps between 3200 - 3800K, gravity from 2.5 - 5.5 - """ - try: - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - except: - # Check atmosphere catalog bounds - (temperature, gravity, metallicity) = get_atmosphere_bounds('merged_BTSettl_phoenix', - metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - sp = pysynphot.Icat('merged_BTSettl_phoenix', temperature, metallicity, gravity) - - # Do some error checking - idx = np.where(sp.flux != 0)[0] - if len(idx) == 0: - print( 'Could not find ATLAS-PHOENIX merge atmosphere model for') - print( ' temperature = %d' % temperature) - print( ' metallicity = %.1f' % metallicity) - print( ' log gravity = %.1f' % gravity) - - return sp - -#---------------------------------------------------------------------# -def get_merged_atmosphere(metallicity=0, temperature=20000, gravity=4.5, verbose=False, - rebin=True): - """ - Return a stellar atmosphere from a suite of different model grids, - depending on the input temperature, (all values in K). - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - - Notes - ----- - The underlying stellar model grid used changes as a function of - stellar temperature (in K): - - * T > 20,000: ATLAS - * 5500 <= T < 20,000: ATLAS - * 5000 <= T < 5500: ATLAS/PHOENIXv16 merge - * 3800 <= T < 5000: PHOENIXv16 - - For T < 3800, there is an additional gravity and metallicity - dependence: - - If T < 3800 and [M/H] = 0: - - * T < 3800, logg < 2.5: PHOENIX v16 - * 3200 <= T < 3800, logg > 2.5: BTSettl_CIFITS2011_2015/PHOENIXV16 merge - * 3200 < T <= 1200, logg > 2.5: BTSettl_CIFITS2011_2015 - - Otherwise, if T < 3800 and [M/H] != 0: - - * T < 3800: PHOENIX v16 - - References: - - * ATLAS: ATLAS9 models (`Castelli & Kurucz 2004 `_) - * PHOENIXv16 (`Husser et al. 2013 `_) - * BTSettl_CIFITS2011_2015: Baraffee+15, Allard+ (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011_2015/SPECTRA/) - - LTE WARNING: - - The ATLAS atmospheres are calculated with LTE, and so they - are less accurate when non-LTE conditions apply (e.g. T > 20,000 - K). Ultimately we'd like to add a non-LTE atmosphere grid for - the hottest stars in the future. - - HOW BOUNDARIES BETWEEN MODELS ARE TREATED: - - At the boundary between two models grids a temperature range is defined - where the resulting atmosphere is a weighted average between the two - grids. Near one boundary one model - is weighted more heavily, while at the other boundary the other - model is weighted more heavily. These are calculated in the - temperature ranges where we switch between model grids, to - ensure a smooth transition. - """ - # For T < 3800, atmosphere depends on metallicity + gravity. - # If solar metallicity, use BTSettl 2015 grid. Only solar metallicity is - # currently available here, so if non-solar metallicity, just stick with - # the Phoenix grid - if (temperature <= 3800) & (metallicity == 0): - # High gravity are in BTSettl regime - if (temperature <= 3200) & (gravity > 2.5): - if verbose: - print( 'BTSettl_2015 atmosphere') - return get_BTSettl_2015_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 3200) & (temperature < 3800) & (gravity > 2.5): - if verbose: - print( 'BTSettl/Phoenixv16 merged atmosphere') - return get_BTSettl_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - # Low gravity is PHOENIX regime - if gravity <= 2.5: - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature <= 3800) & (metallicity != 0): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - # For T > 3800, no metallicity or gravity dependence - if (temperature >= 3800) & (temperature < 5000): - if verbose: - print( 'Phoenixv16 atmosphere') - return get_phoenixv16_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity, - rebin=rebin) - - if (temperature >= 5000) & (temperature < 5500): - if verbose: - print( 'ATLAS/Phoenix merged atmosphere') - return get_atlas_phoenix_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if (temperature >= 5500) & (temperature < 20000): - if verbose: - print( 'ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - if temperature >= 20000: - if verbose: - print( 'Still ATLAS merged atmosphere') - return get_castelli_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - #print('CMFGEN') - #return get_cmfgenRot_atmosphere_closest(metallicity=metallicity, - # temperature=temperature, - # gravity=gravity) - - - - -def get_wd_atmosphere(metallicity=0, temperature=20000, gravity=4, verbose=False): - """ - Return the white dwarf atmosphere from - `Koester et al. 2010 `_. - If desired parameters are - outside of grid, return a blackbody spectrum instead - - Parameters - ---------- - metallicity: float - The stellar metallicity, in terms of [Z] - - temperature: float - The stellar temperature, in units of K - - gravity: float - The stellar gravity, in cgs units - - rebin: boolean - If true, rebins the atmospheres so that they are the same - resolution as the Castelli+04 atmospheres. Default is False, - which is often sufficient synthetic photometry in most cases. - - verbose: boolean - True for verbose output - """ - try: - if verbose: - print('wdKoester atmosphere') - - return get_wdKoester_atmosphere(metallicity=metallicity, - temperature=temperature, - gravity=gravity) - - except pysynphot.exceptions.ParameterOutOfBounds: - # Use a black-body atmosphere. - bbspec = get_bb_atmosphere(temperature=temperature, verbose=verbose) - return bbspec - -def get_bb_atmosphere(metallicity=None, temperature=20_000, gravity=None, - verbose=False, rebin=None, - wave_min=500, wave_max=50_000, wave_num=20_000): - """ - Return a blackbody spectrum - - Parameters - ---------- - temperature: float, default=20_000 - The stellar temperature, in units of K - wave_min: float, default=500 - Sets the minimum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_max: float, default=50_000 - Sets the maximum wavelength (in Angstroms) of the wavelength range - for the blackbody spectrum - wave_num: int, default=20_000 - Sets the number of wavelength points in the wavelength range - Note: the wavelength range is evenly spaced in log space - """ - if ((metallicity is not None) or (gravity is not None) or - (rebin is not None)): - warnings.warn( - 'Only `temperature` keyword is used for black-body atmosphere' - ) - - if verbose: - print('Black-body atmosphere') - - # Modify pysynphot's default waveset to specified bounds - pysynphot.refs.set_default_waveset( - minwave=wave_min, maxwave=wave_max, num=wave_num - ) - - # Get black-body atmosphere for specified temperature from pysynphot - bbspec = pysynphot.spectrum.BlackBody(temperature) - - # pysynphot `BlackBody` generates spectrum in `photlam`, need in `flam` - bbspec.convert('flam') - - # `BlackBody` spectrum is normalized to solar radius star at 1 kiloparsec. - # Need to remove this normalization for SPISEA by multiplying bbspec - # by (1000 * 1 parsec / 1 Rsun)**2 = (1000 * 3.08e18 cm / 6.957e10 cm)**2 - bbspec *= (1000 * 3.086e18 / 6.957e10)**2 - - return bbspec - - -#--------------------------------------# -# Atmosphere formatting functions -#--------------------------------------# - -def download_CMFGEN_atmospheres(Table_rot, Table_norot): - """ - Downloads CMFGEN models from - https://sites.google.com/site/fluxesandcontinuum/home; - these contain continuum as well as lines. - - Table_rot, Table_norot are tables with the file prefixes - and model atmosphere parameters, taken by hand from the - Fierro+15 paper - - Website addresses are hardcoded - - Puts downloaded models in the current working directory. - """ - print( 'WARNING: THIS DOES NOT COMPLETELY WORK') - print( '**********************') - t_rot = Table.read(Table_rot, format='ascii') - t_norot = Table.read(Table_norot, format='ascii') - - tables = [t_rot, t_norot] - filenames = [t_rot['col1'], t_norot['col1']] - - # Hardcoded list of webiste addresses - web_base1 = 'https://sites.google.com/site/fluxesandcontinuum/home/' - web_base2 = 'https://sites.google.com/site/modelsobmassivestars/' - web = [web_base1+'009-solar-masses/',web_base1+'012-solar-masses/', - web_base1+'015-solar-masses/',web_base1+'020-solar-masses/', - web_base1+'025-solar-masses/',web_base2+'009-solar-masses-tracks/', - web_base2+'040-solar-masses/',web_base2+'060-solar-masses/', - web_base1+'085-solar-masses/',web_base1+'120-solar-masses/'] - # Array of masses that matches the website addresses - mass_arr = np.array([9.,12.,15.,20.,25.,32.,40.,60.,85.,120.]) - - # Loop through rotating and unrotating case. First loop is rot, second unrot - for i in range(2): - # Extract masses from filenames - masses = [] - for j in filenames[i]: - tmp = j.split('m') - mass = float(tmp[1][:-1]) - masses.append(mass) - - # Download the models webpage by webpage. A bit tricky because masses - # change slightly within a particular website. THIS IS WHAT FAILS - for j in range(len(web)): - if j == 0: - good = np.where( (masses <= mass_arr[j]) ) - else: - g = j - 1 - good = np.where( (masses <= mass_arr[j]) & - (masses > mass_arr[g]) ) - # Use wget command to pull down the files, and unzip them - for k in good[0]: - full = web[j]+'{1:s}.flx.zip'.format(mass_arr[j],filenames[i][k]) - os.system('wget ' + full) - os.system('unzip '+ filenames[i][k] + '.flx.zip') - - return - -def organize_CMFGEN_atmospheres(path_to_dir): - """ - Organize CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html) - into rot and noRot directories - - path_to_dir is from current working directory to directory - containing the downloaded models. Assumed that models - and tables describing parameters are in this directory. - - Tables describing parameters MUST be named Table_rot.txt, - Table_noRot.txt. Made by hand from Tables 3, 4 in Fierro+15. - These are located in same original directory as atmosphere files - - Will separate files into 2 subdirectories, one rotating and - the other non-rotating - - *Can't have any other files starting with "t" in model directory to start!* - """ - # First, record current working directory to return to later - start_dir = os.getcwd() - - # Enter atmosphere directory, collect rotating and non-rotating - # file names (assumed to all start with "t") - os.chdir(path_to_dir) - rot_models = glob.glob("t*r.flx*") - noRot_models = glob.glob("t*n.flx*") - - # Separate into different subdirectories - if os.path.exists('cmfgenF15_rot'): - pass - else: - os.mkdir('cmfgenF15_rot') - os.mkdir('cmfgenF15_noRot') - - for mod in rot_models: - cmd = 'mv {0:s} cmfgenF15_rot'.format(mod) - os.system(cmd) - - for mod in noRot_models: - cmd = 'mv {0:s} cmfgenF15_noRot'.format(mod) - os.system(cmd) - - # Also move Tables with model parameters into correct directory - os.system('mv Table_rot.txt cmfgenF15_rot') - os.system('mv Table_noRot.txt cmfgenF15_noRot') - - # Return to original directory - os.chdir(start_dir) - - return - -def make_CMFGEN_catalog(path_to_dir): - """ - Create cdbs catalog.fits of CMFGEN grid from Fierro+15 - (http://www.astroscu.unam.mx/atlas/index.html). - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to directory - containing the rotating or non-rotating models (i.e. cmfgenF15_rot). Also, - needs to be a Table*.txt file which contains the parameters for all of the - original models, since params in filename are not precise enough - - Will create catalog.fits file in atmosphere directory with - description of each model - - *Can't have any other files starting with "t" in model directory to start!* - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere - # Note: can't rely on filename for this because not precise enough!! - - #---------OLD: GETTING PARAMS FROM FILENAME-------# - # Collect file names (assumed to all start with "t") - #files = glob.glob("t*") - #for name in files: - # tmp = name.split('l') - # temp = float(tmp[0][1:]) * 100.0 # In kelvin - - # lumtmp = tmp[1].split('_') - # lum = float(lumtmp[0][:-5]) * 1000.0 # In L_sun - - # mass = float(lumtmp[0][5:-1]) # In M_sun - - # Need to calculate log g from T and L (cgs) - # lum_sun = 3.846 * 10**33 # erg/s - # M_sun = 2 * 10**33 # g - # G_si = 6.67 * 10**(-8) # cgs - # sigma_si = 5.67 * 10**(-5) # cgs - - # g = (G_si * mass * M_sun * 4 * np.pi * sigma_si * temp**4) / \ - # (lum * lum_sun) - # logg = np.log10(g) - #---------------------------------------------------# - - # Read table with atmosphere params - table = glob.glob('Table_*') - t = Table.read(table[0], format = 'ascii') - names = t['col1'] - temps = t['col2'] - logg = t['col4'] - - # Create catalog.fits file - index_str = [] - name_str = [] - for i in range(len(names)): - index = '{0:5.0f},0.0,{1:3.2f}'.format(temps[i], logg[i]) - - #---NOTE: THE FOLLOWING DEPENDS ON FINAL LOCATION OF CATALOG FILE---# - #path = path_to_dir + '/' + names[i] - path = names[i] + '.fits[Flux]' - - index_str.append(index) - name_str.append(path) - - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits') - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def cdbs_cmfgen(path_to_dir, path_to_cdbs_dir): - """ - Code to put cmfgen models into cdbs format and adds proper unit keyword in - fits header. Save as fits file - - path_to_dir goes from current directory to cmfgen_rot or cmfgen_norot - directory with the *.flx models. Note that these files have already been - organized using organize_CMFGEN_atmospheres code. - - path_to_cdbs_dir goes from current directory to cdbs/grid/cmfgen_rot or - cmfgen_norot directory. Will copy new fits files to this directory. - This directory must already exist! - """ - # Save starting directory for later, move into path_to_dir directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('*.flx') - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - # Open file, extract useful info - t = Table.read(i, format='ascii') - wave = t['col1'] - flux = t['col2'] # Flux is already in erg/cm^2/s/A - - # Need to eliminate duplicate entries (pysynphot crashes) - unique = np.unique(wave, return_index=True) - wave = wave[unique[1]] - flux = flux[unique[1]] - - # Make fits table from individual columns. - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - #Adding unit keywords - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - prihdu = fits.PrimaryHDU() - - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i[:-4]+'.fits', overwrite=True) - - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Return to original directory, copy over new .fits files to cdbs directory - os.chdir(start_dir) - cmd = 'mv {0:s}/*.fits {1:s}'.format(path_to_dir, path_to_cdbs_dir) - os.system(cmd) - - return - -def rebin_cmfgen(cdbs_path, rot=True): - """ - Rebin cmfgen_rot and cmfgen_norot models to atlas ck04 resolution; - this makes spectrophotometry MUCH faster - - cdbs_path: path to cdbs directory - rot=True for rotating models (cmfgen_rot), False for non-rotating models - - makes new directory in cdbs/grid: cmfgen_rot_rebin or cmfgen_norot_rebin - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing cmfgen model; we will steal the header. - # Also define paths to new rebin directories - if rot == True: - tmp = cdbs_path+'/grid/cmfgen_rot/t0200l0008m009r.fits' - path = cdbs_path+'/grid/cmfgen_rot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_rot/' - else: - tmp = cdbs_path+'/grid/cmfgen_norot/t0200l0007m009n.fits' - path = cdbs_path+'/grid/cmfgen_norot_rebin/' - orig_path = cdbs_path+'/grid/cmfgen_norot/' - - cmfgen_hdu = fits.open(tmp) - header0 = cmfgen_hdu[0].header - # Create rebin directories if they don't already exist. Copy over - # catalog.fits file from original directory (will be the same) - if not os.path.exists(path): - os.mkdir(path) - cmd = 'cp {0:s}catalog.fits {1:s}'.format(orig_path, path) - os.system(cmd) - - # Read in the catalog.fits file - cat = fits.getdata(orig_path + 'catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - # First column in new files will be for [atlas] wavelength - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - - # For each catalog.fits entry, read the unbinned spectrum and rebin to - # the atlas resolution. Make a new fits file in rebin directory - count = 0 - for ff in range(len(files_all)): - count += 1 - # Extract the temp, Z, logg - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - grav = float(vals[2]) - - # Fetch the spectrum - if rot == True: - sp = pysynphot.Icat('cmfgen_rot', temp, metal, grav) - else: - sp = pysynphot.Icat('cmfgen_norot', temp, metal, grav) - - # Rebin - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - # Make the FITS file from the columns with header - cols = fits.ColDefs([c0,c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - # Write hdu to new directory with same filename - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(path+files_all[ff]) - - print( 'Finished file {0} of {1}'.format(count, len(files_all)) ) - return - - -def organize_PHOENIXv16_atmospheres(path_to_dir, met_str='m00'): - """ - Construct the Phoenix Husser+13 atmopsheres for each model. Combines the - fluxes from the *HiRES.fits files and the wavelengths of the - WAVE_PHONEIX-ACES-AGSS-COND-2011.fits file. - - path_to_dir is the path to the directory containing all of the downloaded - files - - met_str is the name of the current metallicity - - Creates new fits files for each atmosphere: phoenix_.fits, - which contains columns for the log g (column header = g#.#). Puts - atmospheres in new directory phoenixm00 - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the current metallicity subdirectory - sub_dir = '../phoenix{0}'.format(met_str) - if os.path.exists(sub_dir): - pass - else: - os.mkdir(sub_dir) - - # Extract wavelength array, make column for later - wavefile = fits.open('WAVE_PHOENIX-ACES-AGSS-COND-2011.fits') - wave = wavefile[0].data - wavefile.close() - wave_col = Column(wave, name = 'WAVELENGTH') - - # Create temp array for Husser+13 grid (given in paper) - temp_arr = np.arange(2300, 7001, 100) - temp_arr = np.append(temp_arr, np.arange(7000, 12001, 200)) - - print( 'Looping though all temps') - # For each temp, build file containing the flux for all gravities - i = 0 - for temp in temp_arr: - files = glob.glob('lte{0:05d}-*-HiRes.fits'.format(temp)) - files.sort() - # Start the table with the wavelength column - t = Table() - t.add_column(wave_col) - for f in files: - # Extract the logg out of filename - logg = f[9:13] - - # Extract fluxes from file - spectrum = fits.open(f) - flux = spectrum[0].data - spectrum.close() - - # Make Column object with fluxes, add to table - col = Column(flux, name = 'g{0:2.1f}'.format(float(logg))) - t.add_column(col) - - # Now, construct final fits file for the given temp - outname = 'phoenix{0}_{1:05d}.fits'.format(met_str, temp) - t.write('{0}/{1}'.format(sub_dir, outname), format = 'fits', overwrite = True) - - # Progress counter for user - i += 1 - print( 'Done {0:d} of {1:d}'.format(i, len(temp_arr))) - - # Return to original directory - os.chdir(start_dir) - return - -def make_PHOENIXv16_catalog(path_to_dir, met_str='m00'): - """ - Makes catalog.fits file for Husser+13 phoenix models. Assumes that - organize_PHOENIXv16_atmospheres has been run already, and that the models lie - in subdirectory phoenix[met_str]. - - path_to_directory is the path to the directory with the reformatted - models (i.e. the output from construct_atmospheres, phoenix[met_str]) - - Puts catalog.fits file in directory the user starts in - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Extract metallicity from metallicity string - met = float(met_str[1]) + (float(met_str[2]) * 0.1) - if 'm' in met_str: - met *= -1. - - # Collect the filenames. Each is a unique temp with many different log g's - files = glob.glob('phoenix*.fits') - files.sort() - - # Create the catalog.fits file, row by row - index_arr = [] - filename_arr = [] - for i in files: - # Get log g values from the column header the file - t = Table.read(i, format='fits') - keys = t.keys() - logg_vals = keys[1:] - - # Extract temp from filename - name = i.split('_') - temp = float(name[1][:-5]) - for j in logg_vals: - logg = float(j[1:]) - index = '{0:5.0f},{1:2.1f},{2:2.1f}'.format(temp, met, logg) - filename = path_to_dir + i + '[' + j + ']' - # Add row to table - index_arr.append(index) - filename_arr.append(filename) - - catalog = Table([index_arr, filename_arr], names=('INDEX', 'FILENAME')) - - # Return to starting directory, write catalog - os.chdir(start_dir) - - if os.path.exists('catalog.fits'): - from astropy.table import vstack - - prev_catalog = Table.read('catalog.fits', format='fits') - joined_catalog = vstack([prev_catalog, catalog]) - - joined_catalog.write('catalog.fits', format='fits', overwrite=True) - else: - catalog.write('catalog.fits', format='fits', overwrite=True) - - return - -def cdbs_PHOENIXv16(path_to_cdbs_dir): - """ - Put the PHOENIXv16 (Husser+13) fits files into cdbs format. This primarily - consists of adjusting the flux units from [erg/s/cm^2/cm] to [erg/s/cm^2/A] - and adding the appropriate keywords to the fits header. - - path_to_cdbs_dir goes from current working directory to phoenix[met] directory - in cdbs/grids/phoenix_v16. Note that these files have already been organized - using organize_PHOENIXv16_atmospheres code. - - Overwrites original files in directory - """ - # Save starting directory for later, move into working directory - start_dir = os.getcwd() - os.chdir(path_to_cdbs_dir) - - # Collect the filenames, make necessary changes to each one - files = glob.glob('phoenix*.fits') - - ## Need to sort filenames; glob doesn't always give them in order - files.sort() - - # Need to make brand-new fits tables with data we want. - counter = 0 - for i in files: - counter += 1 - - # Read in current FITS table - cur_table = Table.read(i, format='fits') - - cur_table.columns[0].name = 'Wavelength' - - num_cols = len(cur_table.colnames) - - # Multiplying each flux column by 10^-8 for conversion - for cur_col_index in range(1, num_cols, 1): - cur_col_name = cur_table.colnames[cur_col_index] - cur_table[cur_col_name] = cur_table[cur_col_name] * 10.**-8 - - - # Construct new FITS file based on old one - hdu = fits.open(i) - header_0 = hdu[0].header - header_1 = hdu[1].header - sci = hdu[1].data - - tbhdu = fits.table_to_hdu(cur_table) - - # Copying over the older headers, adding unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - tbhdu.header['TUNIT3'] = 'FLAM' - tbhdu.header['TUNIT4'] = 'FLAM' - tbhdu.header['TUNIT5'] = 'FLAM' - tbhdu.header['TUNIT6'] = 'FLAM' - tbhdu.header['TUNIT7'] = 'FLAM' - tbhdu.header['TUNIT8'] = 'FLAM' - tbhdu.header['TUNIT9'] = 'FLAM' - tbhdu.header['TUNIT10'] = 'FLAM' - tbhdu.header['TUNIT11'] = 'FLAM' - tbhdu.header['TUNIT12'] = 'FLAM' - tbhdu.header['TUNIT13'] = 'FLAM' - tbhdu.header['TUNIT14'] = 'FLAM' - - # Construct and write out final FITS file - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(i, overwrite=True) - - hdu.close() - print( 'Done {0:2.0f} of {1:2.0f}'.format(counter, len(files))) - - # Change back to starting directory - os.chdir(start_dir) - - return - -def rebin_phoenixV16(cdbs_path): - """ - Rebin phoenixV16 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: phoenix_v16_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - ## (This assumes that at least 'm00' metallicity exists) - tmp = '{0}/grid/phoenix_v16/phoenix{1}/phoenix{1}_02400.fits'.format(cdbs_path, 'm00') - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/phoenix_v16_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/phoenix_v16/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - temp_arr = np.zeros(len(files_all), dtype=float) - logg_arr = np.zeros(len(files_all), dtype=float) - metal_arr = np.zeros(len(files_all), dtype=float) - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - - temp_arr[ff] = float(vals[0]) - metal_arr[ff] = float(vals[1]) - logg_arr[ff] = float(vals[2]) - - - metal_uniq = np.unique(metal_arr) - temp_uniq = np.unique(temp_arr) - - for mm in range(len(metal_uniq)): - metal = metal_uniq[mm] # metallicity - - # Construct str for metallicity (for appropriate directory name) - met_str = str(int(np.abs(metal))) + str(int((metal % 1.0)*10)) - if metal > 0: - met_str = 'p' + met_str - else: - met_str = 'm' + met_str - - # Make directory for current metallicity if it does not exist yet - if not os.path.exists(path + 'phoenix' + met_str): - os.mkdir(path + 'phoenix' + met_str) - - for tt in range(len(temp_uniq)): - temp = temp_uniq[tt] # temperature - - # Pick out the list of gravities for this T, Z combo - idx = np.where((metal_arr == metal) & (temp_arr == temp))[0] - logg_exist = logg_arr[idx] - - # All gravities will go in one file. Here is the output - # file name. - outfile = path + files_all[idx[0]].split('[')[0] - - ## If the rebinned file already exists, continue - if os.path.exists(outfile): - continue - - # Build a columns array. One column for each gravity. - cols_arr = [] - - # Make the wavelength column, which is first in the cols array. - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - cols_arr.append(c0) - - for gg in range(len(logg_exist)): - grav = logg_exist[gg] # gravity - - # Fetch the spectrum - sp = pysynphot.Icat('phoenix_v16', temp, metal, grav) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Store the spectrum - name = 'g{0:3.1f}'.format(grav) - col = fits.Column(name=name, format='E', array=flux_rebin) - cols_arr.append(col) - - - # Make the FITS file from the columns with header. - cols = fits.ColDefs(cols_arr) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - for gg in range(len(logg_exist)): - tbhdu.header['TUNIT{0:d}'.format(gg+2)] = 'FLAM' - - # Write hdu - finalhdu = fits.HDUList([prihdu, tbhdu]) - # don't have overwrite to protect original files. - finalhdu.writeto(outfile) - - print( 'Finished file ' + outfile + ' with gravities: ', logg_exist) - - - return - - -def rebin_spec(wave, specin, wavnew): - """ - Helper routine to rebin spectra. TAKEN FROM ASTROBETTER BLOG FROM JESSICA: - http://www.astrobetter.com/blog/2013/08/12/ - python-tip-re-sampling-spectra-with-pysynphot/ - """ - spec = pysynphot.spectrum.ArraySourceSpectrum(wave=wave, flux=specin) - f = np.ones(len(wave)) - filt = pysynphot.spectrum.ArraySpectralElement(wave, f, waveunits='angstrom') - obs = pysynphot.observation.Observation(spec, filt, binset=wavnew, force='taper') - - return obs.binflux - -def organize_BTSettl_2015_atmospheres(path_to_dir): - """ - Construct cdbs-ready BTSettl_CIFITS_2011_2015 atmospheres for each model. - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/grid/BTSettl_2015 - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # If it doesn't already exist, create the BTSettl subdirectory - if not os.path.exists('BTSettl_2015'): - os.mkdir('BTSettl_2015') - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.spec.fits') - - for i in files: - hdu = fits.open(i) - spec = hdu[1].data - header_0 = hdu[0].header - header_1 = hdu[1].header - - wave = spec.field(0) - flux = spec.field(1) - - # Get units right: convert wave from microns to Angstroms, - # flux from W /m^2/ micron to erg/s/cm^2/A - wave_new = wave * 10**4 - flux_new = flux * 10**(-1) - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave_new) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU(header=header_0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'grid/BTSettl_2015/'+i, overwrite=True) - - hdu.close() - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_BTSettl_2015_catalog(path_to_dir): - """ - Create cdbs catalog.fits of BTSettl_CIFITS2011_2015 grid. - THIS IS STEP 2, after organize_CMFGEN_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*spec.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('-') - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl_2015(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin BTSettle_CIFITS2011_2015 models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: BTSettl_2015_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing phoenix model; we will steal the header - tmp = cdbs_path+'/grid/phoenix_v16/phoenixm00/phoenixm00_02400.fits' - phoenix_hdu = fits.open(tmp) - header0 = phoenix_hdu[0].header - phoenix_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/BTSettl_2015_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/BTSettl_2015/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning BTSettl spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - sp = pysynphot.Icat('BTSettl_2015', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - -def make_wavelength_unique(files, dirname): - """ - Helper function to go through each BTSettl spectrum and ensure that - each wavelength point is unique. This is required for rebinning to work. - - - files: list of files to run this analysis on - """ - # Loop through each file, find fix repeated wavelength entries if necessary - for i in files: - t = Table.read('{0}/{1}'.format(dirname,i), format='fits') - test = np.unique(t['Wavelength'], return_index=True) - - if len(t) != len(test[0]): - t = t[test[1]] - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - # Also make sure wavelength is monotonic. If it is not, then it is - # a sign that the wavelengths are out of order - diff = np.diff(t['Wavelength']) - bad = np.where(diff < 0) - if len(bad[0]) > 0: - t.sort('Wavelength') - - c0 = fits.Column(name='Wavelength', format='D', array=t['Wavelength']) - c1 = fits.Column(name='Flux', format='E', array=t['Flux']) - cols = fits.ColDefs([c0, c1]) - - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto('{0}/{1}'.format(dirname,i), overwrite=True) - - print('Done {0}'.format(i)) - - return - -def organize_BTSettl_atmospheres(): - """ - Construct cdbs-ready atmospheres for the BTSettl grid (CIFITS2011). - The code expects tp be run in cdbs/grid/BTSettl, and expects that the - individual model files have been downloaded from online - (https://phoenix.ens-lyon.fr/Grids/BT-Settl/CIFIST2011/SPECTRA/) - and processed into python-readable ascii files. - """ - orig_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btm10', 'btm05', 'btp00', 'btp05'] - - - # Go through each directory, turning each spectrum into a cdbs-ready file. - # Will convert flux into Ergs/sec/cm**2/A (FLAM) units and save as a fits file, - # for faster access later - for ii in dirs: - print('Starting {0}'.format(ii)) - os.chdir(ii) - - files = glob.glob('*.txt') - count=0 - for jj in files: - t = Table.read(jj, format='ascii') - # First, trim the wavelengths to a more reasonable wavelength range - good = np.where( (t['col1'] > 1000) & (t['col1'] < 70000) ) - t = t[good] - - # Convert flux units to Flam (Ergs/sec/cm**2/A) - flux_new = 10**(t['col2'] - 8.0) - - # Save the file as a fits file - c0 = fits.Column(name='Wavelength', format='D', array=t['col1']) - c1 = fits.Column(name='Flux', format='E', array=flux_new) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Add unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto('{0}.fits'.format(jj[:-4]), overwrite=True) - hdu_new.close() - count += 1 - print('Done {0} of {1}'.format(count, len(files))) - - # Now, clean up all the files made when unzipping the spectra - cmd1 = 'rm *.bz2' - cmd2 = 'rm *.tmp' - #cmd3 = 'rm *.txt' - os.system(cmd1) - os.system(cmd2) - #os.system(cmd3) - print('==============================') - print('Done {0}'.format(ii)) - print('==============================') - - # Go back to original directory, move to next metallicity directory - os.chdir(orig_dir) - - return - -def make_BTSettl_catalog(): - """ - Create cdbs catalog.fits of BTSettl grid. - THIS IS STEP 2, after organize_BTSettl_atmospheres has - been run. - - Code expects to be run in cdbs/grid/BTSettl - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - dirs = ['btm25', 'btm20', 'btm15', 'btm10', 'btm05', 'btp00', 'btp05'] - #dirs = ['btp05'] - - # Construct the catalog.fits file input. The input consists of - # and index string that specifies the stellar paramters, and a - # name string that points to the file - # Loop over all the metallicity directories to construct these inputs - index_str = [] - name_str = [] - for ii in dirs: - os.chdir(ii) - files = glob.glob('*.fits') - - # Construct the metallicity val - if 'm' in ii: - metal_flag = -1 * float(ii[3:])*0.1 - else: - metal_flag = float(ii[3:])*0.1 - - # Now collect the info from the files - for jj in files: - tmp = jj.split('-') - - if metal_flag >= 0: - temp = float(tmp[0].split('+')[0][3:]) * 100.0 # In kelvin - try: - logg = float(tmp[1]) - except: - logg = float(tmp[1].split('+')[0]) - else: - temp = float(tmp[0][3:]) * 100.0 # In kelvin - logg = float(tmp[1]) - - index_str.append('{0},{1},{2:3.2f}'.format(int(temp), metal_flag, logg)) - name_str.append('{0}/{1}[Flux]'.format(ii, jj)) - - # Go back to original directory to move to next metallicity - print('Done {0}'.format(ii)) - os.chdir(start_dir) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_BTSettl(make_unique=False): - """ - Rebin BTSettle models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory: BTSettl_rebin - - Code expects to be run in cdbs/grid directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Create cdbs/grid directory for rebinned models - path = 'BTSettl_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata('BTSettl/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - #==============================# - #tmp = [] - #for ii in files_all: - # if ii.startswith('btp00'): - # tmp.append(ii) - #files_all = tmp - #=============================# - - print( 'Rebinning BTSettl spectra') - if make_unique: - print('Making unique') - make_wavelength_unique(files_all, 'BTSettl') - print('Done') - - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the BTSettl spectrum, rebin flux - try: - sp = pysynphot.Icat('BTSettl', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - except: - pdb.set_trace() - orig_file = '{0}/{1}'.format('BTSettl/', files_all[ff].split('[')[0]) - outfile = path + files_all[ff].split('[')[0] - cmd = 'cp {0} {1}'.format(orig_file, outfile) - os.system(cmd) - - print('Done {0} of {1}'.format(ff, len(files_all))) - - return - -def organize_WDKoester_atmospheres(path_to_dir): - """ - Construct cdbs-ready wdKoester WD atmospheres for each model. (from Koester 2010) - Will convert wavelength units to angstroms and flux units to [erg/s/cm^2/A] - - path_to_dir is the path to the directory containing all of the downloaded - files - - Saves cdbs-ready atmospheres into os.environ['PYSYN_CDBS']/wdKoeseter - (assumes this directory exists) - """ - # Save current directory for return later, move into working dir - start_dir = os.getcwd() - os.chdir(path_to_dir) - - # Process each atmosphere file independently - print( 'Creating cdbs-ready files') - files = glob.glob('*.dk.dat.txt') - - for i in files: - data = Table.read(i, format='ascii') - - wave = data['col1'] # angstrom - flux = data['col2'] # erg/s/cm^2/A - - # Make new fits table - c0 = fits.Column(name='Wavelength', format='D', array=wave) - c1 = fits.Column(name='Flux', format='E', array=flux) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - - # Copy over headers, update unit keywords - prihdu = fits.PrimaryHDU() - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - hdu_new = fits.HDUList([prihdu, tbhdu]) - - # Write new fits table in cdbs directory - hdu_new.writeto(os.environ['PYSYN_CDBS']+'/grid/wdKoester/'+i.replace('.txt', '.fits'), overwrite=True) - - hdu_new.close() - - # Return to original directory - os.chdir(start_dir) - return - -def make_WDKoester_catalog(path_to_dir): - """ - Create cdbs catalog.fits of wdKoester grid. - THIS IS STEP 2, after organize_WDKoester_atmospheres has - been run. - - path_to_dir is from current working directory to the cdbs directory. - Will create catalog.fits file in atmosphere directory with - description of each model - """ - # Record current working directory for later - start_dir = os.getcwd() - - # Enter atmosphere directory - os.chdir(path_to_dir) - - # Extract parameters for each atmosphere from the filename, - # construct columns for catalog file - files = glob.glob("*dk.dat.fits") - index_str = [] - name_str = [] - for name in files: - tmp = name.split('.') - tmp2 = tmp[0].split('_') - temp = float(tmp2[0][2:]) # Kelvin - logg = float(tmp2[1]) / 100.0 # log(g) - - index_str.append('{0:5.0f},0.0,{1:3.2f}'.format(temp, logg)) - name_str.append('{0}[Flux]'.format(name)) - - # Make catalog - catalog = Table([index_str, name_str], names = ('INDEX', 'FILENAME')) - - # Create catalog.fits file in directory with the models - catalog.write('catalog.fits', format = 'fits', overwrite=True) - - # Move back to original directory, create the catalog.fits file - os.chdir(start_dir) - - return - -def rebin_WDKoester(cdbs_path=os.environ['PYSYN_CDBS']): - """ - Rebin wdKoester models to atlas ck04 resolution; this makes - spectrophotometry MUCH faster - - makes new directory in cdbs/grid: wdKoester_rebin - - cdbs_path: path to cdbs directory - """ - # Get an atlas ck04 model, we will use this to set wavelength grid - sp_atlas = get_castelli_atmosphere() - - # Open a fits table for an existing model; we will steal the header - tmp = cdbs_path+'/grid/wdKoester/da70000_800.dk.dat.fits' - wdkoester_hdu = fits.open(tmp) - header0 = wdkoester_hdu[0].header - wdkoester_hdu.close() - - # Create cdbs/grid directory for rebinned models - path = cdbs_path+'/grid/wdKoester_rebin/' - if not os.path.exists(path): - os.mkdir(path) - - # Read in the existing catalog.fits file and rebin every spectrum. - cat = fits.getdata(cdbs_path + '/grid/wdKoester/catalog.fits') - files_all = [cat[ii][1].split('[')[0] for ii in range(len(cat))] - - print( 'Rebinning wdKoester spectra') - for ff in range(len(files_all)): - vals = cat[ff][0].split(',') - temp = float(vals[0]) - metal = float(vals[1]) - logg = float(vals[2]) - - # Fetch the wdKoester spectrum, rebin flux - sp = pysynphot.Icat('wdKoester', temp, metal, logg) - flux_rebin = rebin_spec(sp.wave, sp.flux, sp_atlas.wave) - - # Make new output - c0 = fits.Column(name='Wavelength', format='D', array=sp_atlas.wave) - c1 = fits.Column(name='Flux', format='E', array=flux_rebin) - - cols = fits.ColDefs([c0, c1]) - tbhdu = fits.BinTableHDU.from_columns(cols) - prihdu = fits.PrimaryHDU(header=header0) - tbhdu.header['TUNIT1'] = 'ANGSTROM' - tbhdu.header['TUNIT2'] = 'FLAM' - - outfile = path + files_all[ff].split('[')[0] - finalhdu = fits.HDUList([prihdu, tbhdu]) - finalhdu.writeto(outfile, overwrite=True) - - return - - diff --git a/spisea/evolution.py b/spisea/evolution.py index 56fbae48..40ba9f71 100755 --- a/spisea/evolution.py +++ b/spisea/evolution.py @@ -13,6 +13,9 @@ from spisea.utils import objects from scipy.interpolate import RegularGridInterpolator from spisea import exceptions +import astropy.units as u +import astropy.constants as c +from astropy import coordinates as coords logger = logging.getLogger('evolution') @@ -91,6 +94,7 @@ def __init__(self, model_dir, age_list, mass_list, z_list): self.z_list = z_list self.mass_list = mass_list self.age_list = age_list + self.external_evol = False self.model_version_name = "None" return @@ -1532,6 +1536,361 @@ def format_isochrones(self): # Return to starting directory os.chdir(start_dir) return + +#===========================================# +# COSMIC Breivik+ 2020 - not normal evo model +#===========================================# +class COSMIC(StellarEvolution): + + def __init__(self, BSEDict='default', keep_disrupted_companions=True, keep_COSMIC_tables=False): + if BSEDict == 'default': + self.BSEDict = { + "pts1": 0.001, "pts2": 0.01, "pts3": 0.02, "zsun": 0.02, "windflag": 3, + "eddlimflag": 0, "neta": 0.5, "bwind": 0.0, "hewind": 0.5, "beta": 0.125, + "xi": 0.5, "acc2": 1.5, "LBV_flag": 1, "alpha1": [1.0, 1.0], + "lambdaf": 0.0, "ceflag": 1, "cekickflag": 2, "cemergeflag": 1, + "cehestarflag": 0, "qcflag": 5, + "qcrit_array": [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0], + "kickflag": 5, "sigma": 265.0, "bhflag": 1, "bhsigmafrac": 1.0, + "sigmadiv": -20.0, "ecsn": 2.25, "ecsn_mlow": 1.6, "aic": 1, "ussn": 1, + "polar_kick_angle": 90.0, + "natal_kick_array": [[-100.0, -100.0, -100.0, -100.0, 0.0], [-100.0, -100.0, -100.0, -100.0, 0.0]], + "mm_mu_ns": 400.0, "mm_mu_bh": 200.0, "remnantflag": 4, + "fryer_mass_limit": 0, "mxns": 3.0, "fryer_fmix": 1.0, + "fryer_mcrit_nsbh": 5.75, "rembar_massloss": 0.5, "wd_mass_lim": 1, + "maltsev_mode": 0, "maltsev_fallback": 0.5, "maltsev_pf_prob": 0.1, + "pisn": -2, "ppi_co_shift": 0.0, "ppi_extra_ml": 0.0, "bhspinflag": 0, + "bhspinmag": 0.0, "grflag": 1, "eddfac": 10, "gamma": -2, "don_lim": -1, + "acc_lim": [-1, -1], "smt_periastron_check": 0, "tflag": 1, "ST_tide": 1, + "fprimc_array": [2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0,2.0/21.0], + "ifflag": 1, "wdflag": 1, "epsnov": 0.001, "bdecayfac": 1, + "bconst": 3000, "ck": 1000, "rejuv_fac": 1.0, "rejuvflag": 0, + "bhms_coll_flag": 0, "htpmb": 1, "ST_cr": 1, "rtmsflag": 0 + } + + else: + self.BSEDict = BSEDict + + self.external_evol = True + self.z_solar = 0.02 #0.014 + self.keep_disrupted_companions = keep_disrupted_companions + self.keep_COSMIC_tables = keep_COSMIC_tables + self.model_version_name = "COSMIC" + + + def evolve(self, star_systems, companions, logAge, metallicity): + from cosmic.utils import p_from_a, a_from_p + from cosmic.sample.initialbinarytable import InitialBinaryTable + from cosmic.evolve import Evolve + + companion_system_idxs = companions['system_idx'] + m1s = star_systems['mass'] + m2s = np.zeros(len(star_systems)) + m2s[companion_system_idxs] = companions['mass'] + + a_Rsuns = (10**companions['log_a'])*u.AU.to('Rsun') + porbs = np.zeros(len(star_systems)) + porbs[companion_system_idxs] = p_from_a(a_Rsuns, m1s[companion_system_idxs], m2s[companion_system_idxs]) + + eccs = np.zeros(len(star_systems)) + eccs[companion_system_idxs] = companions['e'] + + kstar1s = (m1s >= 0.7).astype(int) # 1 if MS above 0.7 and 0 if MS below 0.7 + kstar2s = (m2s >= 0.7).astype(int) # 1 if MS above 0.7 and 0 if MS below 0.7 + + binary_pop = InitialBinaryTable.InitialBinaries(m1=m1s, m2=m2s, porb=porbs, + ecc=eccs, tphysf=[10**logAge/1e6]*len(m1s), + kstar1=kstar1s, kstar2=kstar2s, metallicity=[self.z_solar*10**metallicity]*len(m1s)) + + bpp, bcm, initC, kick_info = Evolve.evolve(initialbinarytable=binary_pop, BSEDict=self.BSEDict) + + final_binaries = bcm[bcm['tphys'] > 0] #only gives the first and last idx, so this takes final one + + # Add number for system idx since we're about to manipuluate them a bunch + star_systems['system_idx'] = np.arange(len(star_systems)) + + # Remove systems that don't show up in bcm final (very few) + if len(final_binaries) != len(star_systems): + # Save or append to initC + initC_fail_path = 'initC_fail.csv' + exists = os.path.exists(initC_fail_path) + initC.to_csv( + initC_fail_path, + mode='a' if exists else 'w', + header=not exists, + index=False + ) + + # Save or append failing binary bcm values to table + mask = ~bcm.loc[bcm['tphys'] == 0, 'bin_num'].isin(final_binaries['bin_num']) + failing_binaries = bcm.loc[bcm['tphys'] == 0].loc[mask] + + print('missing binaries', bcm.loc[bcm['tphys'] == 0].loc[mask]) + missing_binary_csv_path = 'missing_cosmic_binaries_bcm.csv' + exists = os.path.exists( missing_binary_csv_path) + failing_binaries.to_csv( + missing_binary_csv_path, + mode='a' if exists else 'w', + header=not exists, + index=False + ) + + # Remove failing binaries from all tables and redefine quantites + bad_bin_nums = failing_binaries['bin_num'] + bad_mask_ss = np.isin(star_systems['system_idx'], bad_bin_nums) + star_systems.remove_rows(np.where(bad_mask_ss)[0]) + + bad_mask_comp = np.isin(companions['system_idx'], bad_bin_nums) + companions.remove_rows(np.where(bad_mask_comp)[0]) + + bpp = bpp[~bpp['bin_num'].isin(bad_bin_nums)] + bcm = bcm[~bcm['bin_num'].isin(bad_bin_nums)] + kick_info = kick_info[~kick_info['bin_num'].isin(bad_bin_nums)] + + # Redefine system idx since companions refer to the positions + star_systems['old_system_idx'] = star_systems['system_idx'] + star_systems['system_idx'] = np.arange(len(star_systems)) + idx_map = dict(zip(star_systems['old_system_idx'], star_systems['system_idx'])) + companions['system_idx'] = np.array([idx_map[i] for i in companions['system_idx']]) + + # redefine the bin_num to the new values too + bpp['bin_num'] = bpp['bin_num'].map(idx_map) + bcm['bin_num'] = bcm['bin_num'].map(idx_map) + kick_info['bin_num'] = kick_info['bin_num'].map(idx_map) + + assert(set(companions['system_idx']) - set(star_systems['system_idx']) == set()) + assert(set(bpp['bin_num']) - set(star_systems['system_idx']) == set()) + assert(set(bcm['bin_num']) - set(star_systems['system_idx']) == set()) + assert(set(kick_info['bin_num']) - set(star_systems['system_idx']) == set()) + + # reset the index to the bin_num column + bpp = bpp.set_index('bin_num', drop=False) + bcm = bcm.set_index('bin_num', drop=False) + kick_info = kick_info.set_index('bin_num', drop=False) + + companion_system_idxs = companions['system_idx'] + m1s = star_systems['mass'] + m2s = np.zeros(len(star_systems)) + m2s[companion_system_idxs] = companions['mass'] + + a_Rsuns = (10**companions['log_a'])*u.AU.to('Rsun') + porbs = np.zeros(len(star_systems)) + porbs[companion_system_idxs] = p_from_a(a_Rsuns, m1s[companion_system_idxs], m2s[companion_system_idxs]) + + eccs = np.zeros(len(star_systems)) + eccs[companion_system_idxs] = companions['e'] + + kstar1s = (m1s >= 0.7).astype(int) # 1 if MS above 0.7 and 0 if MS below 0.7 + kstar2s = (m2s >= 0.7).astype(int) # 1 if MS above 0.7 and 0 if MS below 0.7 + + final_binaries = bcm[bcm['tphys'] > 0] #only gives the first and last idx, so this takes final one + + print("WARNING: Some binaries didn't make it. Something went wrong with COSMIC. Saved to initC_fail.csv and missing_cosmic_binaries_bcm.csv") + + # initializes kick columns with zeros + star_systems['kick_x'] = 0 + star_systems['kick_y'] = 0 + star_systems['kick_z'] = 0 + + # rotates output kicks from cosmic to inclination + # picks random direction for singles + inclinations = np.arccos(2 * np.random.rand(len(star_systems)) - 1.0) # initializes random inclinations in radians + existing_inclinations = np.deg2rad(companions['i']) + inclinations[companion_system_idxs] = existing_inclinations + inclinations = np.repeat(inclinations, 2) # accounts for two rows per system in kick table + rotated_kick_values_1 = self.get_kick_differential(kick_info['delta_vsysx_1'], kick_info['delta_vsysy_1'], + kick_info['delta_vsysz_1'], inclination = inclinations) + rotated_kick_values_2 = self.get_kick_differential(kick_info['delta_vsysx_2'], kick_info['delta_vsysy_2'], + kick_info['delta_vsysz_2'], inclination = inclinations) + kick_info['delta_vsysx_1_rot'] = rotated_kick_values_1.d_x + kick_info['delta_vsysy_1_rot'] = rotated_kick_values_1.d_y + kick_info['delta_vsysz_1_rot'] = rotated_kick_values_1.d_z + + kick_info['delta_vsysx_2_rot'] = rotated_kick_values_2.d_x + kick_info['delta_vsysy_2_rot'] = rotated_kick_values_2.d_y + kick_info['delta_vsysz_2_rot'] = rotated_kick_values_2.d_z + + + star_systems['mass_current'] = final_binaries['mass_1'] + star_systems['Teff'] = final_binaries['teff_1'] + star_systems['L'] = final_binaries['lum_1'] + star_systems['logg'] = self.calc_logg(final_binaries['mass_1'], final_binaries['rad_1']) + + # Takes sum of the delta kicks in case there was a kick, no disruption, then second kick + # Even for isolated stars, take sum since second row is blank + primary_kick_sum = ( + kick_info + .groupby(level=0)[["delta_vsysx_1_rot", "delta_vsysy_1_rot", "delta_vsysz_1_rot"]] + .sum() + ) + star_systems["kick_x"] = primary_kick_sum["delta_vsysx_1_rot"].reindex(star_systems["system_idx"], fill_value=0).to_numpy() + star_systems["kick_y"] = primary_kick_sum["delta_vsysy_1_rot"].reindex(star_systems["system_idx"], fill_value=0).to_numpy() + star_systems["kick_z"] = primary_kick_sum["delta_vsysz_1_rot"].reindex(star_systems["system_idx"], fill_value=0).to_numpy() + + companions['mass_current'] = final_binaries['mass_2'][companion_system_idxs] + companions['Teff'] = final_binaries['teff_2'][companion_system_idxs] + companions['L'] = final_binaries['lum_2'][companion_system_idxs] + companions['logg'] = self.calc_logg(final_binaries['mass_2'][companion_system_idxs], final_binaries['rad_2'][companion_system_idxs]) + # Also take sum of companion kicks + companion_kick_sum = ( + kick_info + .groupby(level=0)[["delta_vsysx_2_rot", "delta_vsysy_2_rot", "delta_vsysz_2_rot"]] + .sum() + ) + companions['kick_x'] = companion_kick_sum["delta_vsysx_2_rot"].reindex(companion_system_idxs, fill_value=0).to_numpy() + companions['kick_y'] = companion_kick_sum["delta_vsysy_2_rot"].reindex(companion_system_idxs, fill_value=0).to_numpy() + companions['kick_z'] = companion_kick_sum["delta_vsysz_2_rot"].reindex(companion_system_idxs, fill_value=0).to_numpy() + + loga = np.log10(final_binaries['sep'][companion_system_idxs]*u.Rsun.to('AU')) + companions['log_a'] = loga + + fixed_phases1 = final_binaries['kstar_1'].to_numpy(copy=True) + fixed_phases1[np.where((final_binaries['kstar_1'] >= 10) & (final_binaries['kstar_1'] <= 12))[0]] = 101 + fixed_phases1[np.where(final_binaries['kstar_1'] == 13)[0]] = 102 + fixed_phases1[np.where(final_binaries['kstar_1'] == 14)[0]] = 103 + star_systems['phase'] = fixed_phases1 + + fixed_phases2 = final_binaries['kstar_2'][companion_system_idxs].to_numpy(copy=True) + fixed_phases2[np.where((final_binaries['kstar_2'][companion_system_idxs] >= 10) & (final_binaries['kstar_2'][companion_system_idxs] <= 12))[0]] = 101 + fixed_phases2[np.where(final_binaries['kstar_2'][companion_system_idxs] == 13)[0]] = 102 + fixed_phases2[np.where(final_binaries['kstar_2'][companion_system_idxs] == 14)[0]] = 103 + companions['phase'] = fixed_phases2 + + # maybe add WR designation + + + # Take the disrupted binaries and put the companions into the star_system table (if desired) + # don't include massless remnant companions + disrupted_binary_companion_idxs = np.where((final_binaries['bin_state'][companion_system_idxs] == 2) & (final_binaries['kstar_2'][companion_system_idxs] != 15))[0] + disrupted_binary_companions_num = 0 + if self.keep_disrupted_companions and len(disrupted_binary_companion_idxs) > 0: + disrupted_binary_companions = companions[disrupted_binary_companion_idxs] + disrupted_binary_companions['systemMass'] = disrupted_binary_companions['mass_current'] + disrupted_binary_companions['isMultiple'] = [False]*len(disrupted_binary_companions) + disrupted_binary_companions['N_companions'] = [0]*len(disrupted_binary_companions) + disrupted_binary_companions.remove_columns(['system_idx', 'log_a', 'e', 'i', 'Omega', 'omega']) + disrupted_binary_companions['system_idx'] = np.arange(len(disrupted_binary_companions)) + len(star_systems) + star_systems = vstack([star_systems, disrupted_binary_companions]) + disrupted_binary_companions_num = len(disrupted_binary_companions) + + #Drop merged companions and totally disappeared systems + # Also promote companions to primaries when the initial primary "merged" (if desired) + mr_companion_only_idxs = np.where((final_binaries['kstar_1'][companion_system_idxs] != 15) & (final_binaries['kstar_2'][companion_system_idxs] == 15))[0] #mr for massless remnant + disappeared_system_companion_idxs = np.where((final_binaries['kstar_1'][companion_system_idxs] == 15) & (final_binaries['kstar_2'][companion_system_idxs] == 15))[0] + companions_to_mr_primaries_idxs = np.where((final_binaries['kstar_1'][companion_system_idxs] == 15) & (final_binaries['kstar_2'][companion_system_idxs] != 15))[0] + + mr_primary_only_idx = np.where((final_binaries['kstar_1'] == 15) & (final_binaries['kstar_2'] != 15))[0] #mr for massless remnant + disappeared_system_primaries = np.where((final_binaries['kstar_1'] == 15) & (final_binaries['kstar_2'] == 15))[0] + + + delete_primary_idxs = np.concatenate((mr_primary_only_idx, disappeared_system_primaries)) + delete_companion_idxs = np.concatenate((disrupted_binary_companion_idxs, mr_companion_only_idxs, disappeared_system_companion_idxs, companions_to_mr_primaries_idxs)) + primaries_to_deleted_companion_idxs = companions[delete_companion_idxs]['system_idx'] + + #Fix binary specification of primaries that lost their companions + #star_systems['isMultiple'][primaries_to_deleted_companion_idxs] = False + #star_systems['N_companions'][primaries_to_deleted_companion_idxs] = 0 + + mask = np.isin(star_systems['system_idx'], + primaries_to_deleted_companion_idxs) + + star_systems['N_companions'][mask] = 0 + star_systems['isMultiple'][mask] = False + + # Promote the companions to merged primaries to primaries + if self.keep_disrupted_companions and len(companions_to_mr_primaries_idxs) > 0: + companions_to_mr_primaries = companions[companions_to_mr_primaries_idxs] + companions_to_mr_primaries['systemMass'] = companions_to_mr_primaries['mass_current'] + companions_to_mr_primaries['isMultiple'] = [False]*len(companions_to_mr_primaries) + companions_to_mr_primaries['N_companions'] = [0]*len(companions_to_mr_primaries) + companions_to_mr_primaries.remove_columns(['system_idx', 'log_a', 'e', 'i', 'Omega', 'omega']) + companions_to_mr_primaries['system_idx'] = np.arange(len(companions_to_mr_primaries)) + len(star_systems) + disrupted_binary_companions_num + star_systems = vstack([star_systems, companions_to_mr_primaries]) + + star_systems.remove_rows(delete_primary_idxs) + companions.remove_rows(delete_companion_idxs) #if kstar 1 is 15 take the seocnd star and if kstar2 is 15 take the other + + #Reassign system_idx vals + star_systems['system_idx_new'] = np.arange(len(star_systems)) + mapping = np.empty(star_systems['system_idx'].max() + 1, dtype=int) + mapping[star_systems['system_idx']] = star_systems['system_idx_new'] + companions['system_idx'] = mapping[companions['system_idx']] + star_systems.remove_columns(['system_idx', 'system_idx_new']) + + # Preserve a scalar kick magnitude alongside the vector components. + for table in (star_systems, companions): + table['kick'] = np.sqrt(table['kick_x']**2 + table['kick_y']**2 + table['kick_z']**2) + + # Make sure we didn't break anything by manipulating the number of companions + assert star_systems['N_companions'].sum() == len(companions) + + if self.keep_COSMIC_tables: + self.bpp = bpp + self.bcm = bcm + self.initC = initC + self.kick_info = kick_info + + return star_systems, companions + + + def calc_logg(self, masses, radii): + """ + Inputs + ------ + masses : array-like + Masses of objects in Msun + + radii : array-like + Radii of stars in Rsun + + Returns + ------- + logg : array-like + Log10 surface gravity in cgs + """ + return np.log10(((np.array(c.G.to('Rsun^3/(Msun*s^2)').value*masses/((radii)**2))*u.Rsun/u.s**2).to('cm/s^2')).value) + + def get_kick_differential(self, delta_v_sys_x, delta_v_sys_y, delta_v_sys_z, phase=None, inclination=None): + """Calculate the :class:`~astropy.coordinates.CylindricalDifferential` from a combination of the natal + kick, Blauuw kick and orbital motion. + + via cogsworth https://github.com/TomWagg/cogsworth/blob/main/cogsworth/kicks.py + + Parameters + ---------- + delta_v_sys_x : :class:`~astropy.units.Quantity` [velocity] + Change in systemic velocity due to natal and Blauuw kicks in BSE :math:`(v_x, v_y, v_z)` frame + (see Fig A1 of `Hurley+02 `_) + delta_v_sys_y : :class:`~astropy.units.Quantity` [velocity] + Change in systemic velocity due to natal and Blauuw kicks in BSE :math:`(v_x, v_y, v_z)` frame + (see Fig A1 of `Hurley+02 `_) + delta_v_sys_z : :class:`~astropy.units.Quantity` [velocity] + Change in systemic velocity due to natal and Blauuw kicks in BSE :math:`(v_x, v_y, v_z)` frame + (see Fig A1 of `Hurley+02 `_) + phase : np.array + Orbital phase angle in radians + inclination : np.array + Inclination to the Galactic plane in radians + + Returns + ------- + kick_differential : :class:`~astropy.coordinates.CylindricalDifferential` + Kick differential + """ + # orbital phase angle and inclination to Galactic plane + thetas = np.random.uniform(0, 2 * np.pi, size = len(delta_v_sys_x)) if phase is None else phase + phis = np.arccos(2 * np.random.rand(len(delta_v_sys_x)) - 1.0) if inclination is None else inclination + + # rotate BSE (v_x, v_y, v_z) into Galactocentric (v_X, v_Y, v_Z) + v_X = delta_v_sys_x * np.cos(thetas) - delta_v_sys_y * np.sin(thetas) * np.cos(phis)\ + + delta_v_sys_z * np.sin(thetas) * np.sin(phis) + v_Y = delta_v_sys_x * np.sin(thetas) + delta_v_sys_y * np.cos(thetas) * np.cos(phis)\ + - delta_v_sys_z * np.cos(thetas) * np.sin(phis) + v_Z = delta_v_sys_y * np.sin(phis) + delta_v_sys_z * np.cos(phis) + kick_differential = coords.CartesianDifferential(v_X, v_Y, v_Z) + + return kick_differential + #==============================# # Merged model classes diff --git a/spisea/synthetic.py b/spisea/synthetic.py index 29d877b2..b1156a03 100755 --- a/spisea/synthetic.py +++ b/spisea/synthetic.py @@ -1,6 +1,7 @@ import os import time import math +import datetime import scipy import scipy.interpolate import inspect @@ -19,11 +20,13 @@ from astropy import constants, units from astropy.table import Table, Column import astropy.modeling +import sys default_evo_model = evolution.MISTv1() default_red_law = reddening.RedLawNishiyama09() default_atm_func = atm.get_merged_atmosphere default_wd_atm_func = atm.get_wd_atmosphere +default_multiplicity = multiplicity.MultiplicityResolvedDK(CSF_max = 1, companion_max = True) def Vega(): # Use Vega as our zeropoint... assume V=0.03 mag and all colors = 0.0 @@ -179,6 +182,9 @@ def __init__(self, iso, imf, cluster_mass, ifmr=None, verbose=True, seed=None, keep_low_mass_stars=False): Cluster.__init__(self, iso, imf, cluster_mass, ifmr=ifmr, verbose=verbose, seed=seed) + + c = constants + # Provide a user warning is random seed is set if seed is not None and verbose: print('WARNING: random seed set to %i' % seed) @@ -193,20 +199,34 @@ def __init__(self, iso, imf, cluster_mass, ifmr=None, verbose=True, # print('IMF sampling took {0:f} s.'.format(end0 - start0)) # Figure out the filters we will make. - self.filt_names = self.set_filter_names() - self.cluster_mass = cluster_mass + try: + self.filt_names = self.set_filter_names() + except: + self.filt_names = iso.filters + self.cluster_mass = cluster_mass #FIXME? + + # Check if using an external evolution model (i.e. COSMIC) + self.external_evol = getattr(iso, 'external_evol', False) ##### # Make isochrone interpolators ##### - interp_keys = ['Teff', 'L', 'logg', 'isWR', 'mass_current', 'phase'] + self.filt_names - self.iso_interps = {} - for ikey in interp_keys: - # self.iso_interps[ikey] = interpolate.interp1d(self.iso.points['mass'], self.iso.points[ikey], - # kind='linear', bounds_error=False, fill_value=np.nan) - self.iso_interps[ikey] = Interpolator(self.iso.points['mass'], self.iso.points[ikey]) - - ##### + if self.external_evol == False: + interp_keys = ['Teff', 'L', 'logg', 'isWR', 'mass_current', 'phase'] + self.filt_names + self.iso_interps = {} + for ikey in interp_keys: + self.iso_interps[ikey] = Interpolator(self.iso.points['mass'], self.iso.points[ikey]) + else: + from scipy.interpolate import LinearNDInterpolator + self.iso.points.sort(['Teff', 'logg', 'metallicity']) + interp_keys = self.filt_names + self.iso_interps = {} + for ikey in interp_keys: + self.iso_interps[ikey] = LinearNDInterpolator((self.iso.points['Teff'], self.iso.points['logg'], + self.iso.points['metallicity']), self.iso.points[ikey], + fill_value=np.nan) + + ##### # Make a table to contain all the information about each stellar system. ##### # start1 = time.time() @@ -216,7 +236,9 @@ def __init__(self, iso, imf, cluster_mass, ifmr=None, verbose=True, # Trim out bad systems; specifically, stars with masses outside those provided # by the model isochrone (except for compact objects). - star_systems, compMass = self._remove_bad_systems(star_systems, compMass, keep_low_mass_stars) + # Assumes external evolution software (i.e. COSMIC) will handle systems that fall outside of range + if self.external_evol == False: + star_systems, compMass = self._remove_bad_systems(star_systems, compMass, keep_low_mass_stars) ##### # Make a table to contain all the information about companions. @@ -226,6 +248,57 @@ def __init__(self, iso, imf, cluster_mass, ifmr=None, verbose=True, star_systems, companions = self._make_companions_table(star_systems, compMass) # end3 = time.time() # print('Companion table new took {0:f} s.'.format(end3 - start3)) + + # Assigns atmospheres based on grid for external evolution software + # Must be done here instead of in Isochrone() since the systems are evolved after generation above + if self.external_evol: + star_systems, companions = iso.evo_model.evolve(star_systems, companions, iso.logAge, iso.metallicity) + + for filt in self.filt_names: + filt_name = filt.split('_') + filt_val = get_filter_info(get_obs_str(filt), rebin=False, vega=vega) + + # Rescale magnitudes to correct radius + # Since original grid was done assuming 1 Rsun + star_systems[filt] = self.iso_interps[filt](star_systems['Teff'], star_systems['logg'], iso.metallicity) + flux_val = filt_val.flux0*(10**(-(star_systems[filt] - filt_val.mag0)/2.5)) + R_vals = np.sqrt((star_systems['L']*(units.Lsun)/(4*np.pi*c.sigma_sb.cgs*(star_systems['Teff']*units.K)**4)).to('pc^2')).value + flux_rescaled = flux_val*((R_vals / iso.distance)**2)/((float(1*units.Rsun.to('pc')) / iso.distance)**2) + m_rescaled = -2.5*np.log10(flux_rescaled/filt_val.flux0) + filt_val.mag0 + star_systems[filt] = m_rescaled + + companions[filt] = self.iso_interps[filt](companions['Teff'], companions['logg'], iso.metallicity) + flux_val = filt_val.flux0*(10**(-(companions[filt] - filt_val.mag0)/2.5)) + R_vals = np.sqrt((companions['L']*(units.Lsun)/(4*np.pi*c.sigma_sb.cgs*(companions['Teff']*units.K)**4)).to('pc^2')).value + flux_rescaled = flux_val*((R_vals / iso.distance)**2)/((float(1*units.Rsun.to('pc')) / iso.distance)**2) + m_rescaled = -2.5*np.log10(flux_rescaled/filt_val.flux0) + filt_val.mag0 + companions[filt] = m_rescaled + + # Add companions masses to primaries + N_comp_max = np.max(star_systems['N_companions']) + comp_index = np.zeros((len(star_systems), N_comp_max), dtype=int) + kk = 0 + for ii in range(len(star_systems)): + for cc in range(star_systems['N_companions'][ii]): + comp_index[ii][cc] = kk + kk += 1 + + # Find all the systems with at least one companion... add the flux + # of that companion to the primary. Repeat for 2 companions, + # 3 companions, etc. + for cc in range(1, N_comp_max+1): + # All systems with at least cc companions. + idx = np.where(star_systems['N_companions'] >= cc)[0] + + # Get the location in the companions array for each system and + # the cc'th companion. + cdx = comp_index[idx, cc-1] + star_systems = self._calc_system_mag(star_systems, companions, idx, cdx, filt) + + #star_systems, companions = self._remove_bad_systems_and_companions(star_systems, companions) + self.companions = companions + + if self.imf.make_multiples and not hasattr(self, 'companions'): self.companions = companions ##### @@ -255,48 +328,59 @@ def _make_star_systems_table(self, mass, isMulti, sysMass): names=['mass', 'isMultiple', 'systemMass']) N_systems = len(star_systems) - # Use our pre-built interpolators to fetch values from the isochrone for each star. - for key in ['Teff', 'L', 'logg', 'mass_current']: - star_systems.add_column(Column(self.iso_interps[key](star_systems['mass']), name=key)) - - # Treat out-of-range mass as isWR=True - star_systems.add_column(Column(~(self.iso_interps['isWR'](star_systems['mass']) < 0.5), name='isWR')) - star_systems.add_column(Column(np.round(self.iso_interps['phase'](star_systems['mass'])), name='phase')) - + # Add columns for the Teff, L, logg, isWR, mass_current, phase, and filters. + for key in ['Teff', 'L', 'logg', 'mass_current', 'phase']: + star_systems.add_column(Column(np.empty(N_systems, dtype=float), name=key)) + star_systems.add_column(Column(np.zeros(N_systems, dtype=bool), name='isWR')) # for models with no WR designation, this remains 0 star_systems['metallicity'] = np.ones(N_systems) * self.iso.metallicity # Add the filter columns to the table. They are empty so far. # Keep track of the filter names in : filt_names for filt in self.filt_names: - star_systems.add_column(Column(self.iso_interps[filt](star_systems['mass']), name=filt)) + star_systems.add_column(Column(np.empty(N_systems, dtype=float), name=filt)) - # For a very small fraction of stars, the star phase falls on integers in-between - # the ones we have definition for, as a result of the interpolation. For these - # stars, round phase down to nearest defined phase (e.g., if phase is 71, - # then round it down to 5, rather than up to 101). - # Note: this only becomes relevant when the cluster is > 10**6 M-sun, this - # effect is so small - # Convert nan_to_num to avoid errors on greater than, less than comparisons - - # Define brown dwarf mass range - bd_mask = (star_systems['mass'] >= 0.01) & (star_systems['mass'] <= 0.08) - #hard code BDs as 90 and invariant masses - star_systems['phase'][bd_mask] = 90 - star_systems['mass_current'][bd_mask] = star_systems['mass'][bd_mask] + # Use our pre-built interpolators to fetch values from the isochrone for each star. + if self.external_evol == False: + star_systems['Teff'] = self.iso_interps['Teff'](star_systems['mass']) + star_systems['L'] = self.iso_interps['L'](star_systems['mass']) + star_systems['logg'] = self.iso_interps['logg'](star_systems['mass']) + star_systems['isWR'] = ~(self.iso_interps['isWR'](star_systems['mass']) < 0.5) #round to 0 or 1 for speed + star_systems['mass_current'] = self.iso_interps['mass_current'](star_systems['mass']) + star_systems['phase'] = np.round(self.iso_interps['phase'](star_systems['mass'])) + star_systems['metallicity'] = np.ones(N_systems)*self.iso.metallicity + + # For a very small fraction of stars, the star phase falls on integers in-between + # the ones we have definition for, as a result of the interpolation. For these + # stars, round phase down to nearest defined phase (e.g., if phase is 71, + # then round it down to 5, rather than up to 101). + # Note: this only becomes relevant when the cluster is > 10**6 M-sun, this + # effect is so small + # Convert nan_to_num to avoid errors on greater than, less than comparisons + + # Define brown dwarf mass range + bd_mask = (star_systems['mass'] >= 0.01) & (star_systems['mass'] <= 0.08) + # hard code BDs as 90 and invariant masses + star_systems['phase'][bd_mask] = 90 + star_systems['mass_current'][bd_mask] = star_systems['mass'][bd_mask] + + # Identify bad phases (non-brown-dwarfs only) + star_systems_phase_non_nan = np.nan_to_num(star_systems['phase'], nan=-99) + bad = np.where( + (star_systems_phase_non_nan > 5) & + (star_systems_phase_non_nan < 101) & + (star_systems_phase_non_nan != 9) & + (star_systems_phase_non_nan != 90) & + (star_systems_phase_non_nan != -99) + ) + # Print warning, if desired + verbose=False + if verbose: + for ii in range(len(bad[0])): + print('WARNING: changing phase {0} to 5'.format(star_systems['phase'][bad[0][ii]])) + star_systems['phase'][bad] = 5 - # Identify bad phases (non-brown-dwarfs only) - star_systems_phase_non_nan = np.nan_to_num(star_systems['phase'], nan=-99) - bad = np.where( - (star_systems_phase_non_nan > 5) & - (star_systems_phase_non_nan < 101) & - (star_systems_phase_non_nan != 9) & - (star_systems_phase_non_nan != 90) & # exclude BD phase - (star_systems_phase_non_nan != -99) - ) - star_systems['phase'][bad] = 5 - - for filt in self.filt_names: - star_systems[filt] = self.iso_interps[filt](star_systems['mass']) + for filt in self.filt_names: + star_systems[filt] = self.iso_interps[filt](star_systems['mass']) ##### # Make Remnants @@ -305,7 +389,7 @@ def _make_star_systems_table(self, mass, isMulti, sysMass): # # Remnants have flux = 0 in all bands if they are generated here. ##### - if self.ifmr != None: + if self.ifmr != None and self.external_evol == False: # Identify compact objects as those with Teff = 0 or with phase > 100 or BDs highest_mass_iso = self.iso.points['mass'].max() idx_rem = np.where((np.isnan(star_systems['Teff'])) & (star_systems['mass'] > highest_mass_iso))[0] @@ -365,13 +449,21 @@ def _make_companions_table(self, star_systems, compMass): ) companions['mass'] = compMass.compressed() + for key in ['Teff', 'L', 'logg', 'mass_current', 'phase']: + companions[key] = np.empty(N_comp_tot, dtype=float) + companions['isWR'] = np.zeros(N_comp_tot, dtype=bool) + companions['metallicity'] = np.ones(N_comp_tot) * self.iso.metallicity + for filt in self.filt_names: + companions[filt] = np.empty(N_comp_tot, dtype=float) + + if self.external_evol: + return star_systems, companions + for key in ['Teff', 'L', 'logg', 'mass_current']: companions[key] = self.iso_interps[key](companions['mass']) - for key in ['isWR', 'phase']: - companions[key] = np.round(self.iso_interps[key](companions['mass'])) - - companions['metallicity'] = np.ones(N_comp_tot) * self.iso.metallicity + companions['isWR'] = ~(self.iso_interps['isWR'](companions['mass']) < 0.5) #round to 0 or 1 for speed + companions['phase'] = np.round(self.iso_interps['phase'](companions['mass'])) bd_mask = (companions['mass'] >= 0.01) & (companions['mass'] <= 0.08) companions['phase'][bd_mask] = 90 @@ -441,6 +533,56 @@ def _make_companions_table(self, star_systems, compMass): return star_systems, companions + + def _calc_system_mag(self, star_systems, companions, idx, cdx, filt): + """ + Helper function to calculate the system magnitude from + companion and primary magnitude. + + Parameters + ---------- + star_systems : Astropy table + Star system table. + + companions: Astropy table + Companions table. + + idx : array-like + Indices of primaries with companions + + cdx : array-like + Indices of companions + + filt : str + Filter name + + Returns + ------- + star_systems : Astropy table + Star system table with system magnitdes corrected + """ + mag_s = star_systems[filt][idx] + mag_c = companions[filt][cdx] + + # Add companion flux to system flux. + f1 = 10**(-mag_s / 2.5) + f2 = 10**(-mag_c / 2.5) + + # For dark objects, turn the np.nan fluxes into zeros. + f1 = np.nan_to_num(f1) + f2 = np.nan_to_num(f2) + + # If *both* objects are dark, then keep the magnitude + # as np.nan. Otherwise, add fluxes together + good = np.where( (f1 != 0) | (f2 != 0) )[0] + bad = np.where( (f1 == 0) & (f2 == 0) )[0] + + star_systems[filt][idx[good]] = -2.5 * np.log10(f1[good] + f2[good]) + star_systems[filt][idx[bad]] = np.nan + + return star_systems + + def _remove_bad_systems(self, star_systems, compMass, keep_low_mass_stars): """ Helper function to remove stars with masses outside the isochrone @@ -477,7 +619,7 @@ def _remove_bad_systems(self, star_systems, compMass, keep_low_mass_stars): (star_systems_teff_non_nan > 0) | (star_systems_phase_non_nan >= 101) ) idx = on_grid | above_grid - + elif self.ifmr == None: print('Remove compact objects, keep low mass stars below grid') # Keep stars (with Teff) and objects below mass grid @@ -1287,6 +1429,350 @@ def plot_mass_magnitude(self, mag, savefile=None): return +class IsochronePhotExternalEvolution(IsochronePhot): + """ + Make an isochrone with synthetic photometry in various filters. + Load from file if possible. + This is for evo_models that do NOT have a grid of isochrones + but rather have their own evolution modules (i.e. COSMIC) + + Parameters + ---------- + logAge : float + The age of the isochrone, in log(years) + + AKs : float + The total extinction in Ks filter, in magnitudes + + distance : float + The distance of the isochrone, in pc + + metallicity : float, optional + The metallicity of the isochrone, in [M/H]. + Default is 0. + + evo_model: model evolution class, optional + Set the stellar evolution model class. + Default is evolution.COSMIC(). + + atm_func: model atmosphere function, optional + Set the stellar atmosphere models for the stars. + Default is atmospheres.get_merged_atmosphere. + + wd_atm_func: white dwarf model atmosphere function, optional + Set the stellar atmosphere models for the white dwafs. + Default is atmospheres.get_wd_atmosphere + + red_law : reddening law object, optional + Define the reddening law for the synthetic photometry. + Default is reddening.RedLawNishiyama09(). + + iso_dir : path, optional + Path to isochrone directory. Code will check isochrone + directory to see if isochrone file already exists; if it + does, it will just read the isochrone. If the isochrone + file doesn't exist, then save isochrone to the isochrone + directory. + + mass_sampling : int, optional + Sample the raw isochrone every `mass_sampling` steps. The default + is mass_sampling = 0, which is the native isochrone mass sampling + of the evolution model. + + wave_range : list, optional + length=2 list with the wavelength min/max of the final spectra. + Units are Angstroms. Default is [3000, 52000]. + + min_mass : float or None, optional + If float, defines the minimum mass in the isochrone. + Unit is solar masses. Default is None + + max_mass : float or None, optional + If float, defines the maxmimum mass in the isochrone. + Units is solar masses. Default is None. + + rebin : boolean, optional + If true, rebins the atmospheres so that they are the same + resolution as the Castelli+04 atmospheres. Default is True, + which is often sufficient synthetic photometry in most cases. + + recomp : boolean, optional + If true, recalculate the isochrone photometry even if + the savefile exists. You should recompute anytime you change + the filter set (see filters below). + + filters : array of strings, optional + Define what filters the synthetic photometry + will be calculated for, via the filter string + identifier. + """ + def __init__(self, logAge, AKs, distance, + metallicity=0.0, + evo_model=default_evo_model, atm_func=default_atm_func, + wd_atm_func = default_wd_atm_func, + wave_range=[3000, 52000], + red_law=default_red_law, mass_sampling=1, atm_grid_dir='./', + min_mass=None, max_mass=None, rebin=True, recomp=False, + filters=['ubv,U', 'ubv,B', 'ubv,V', + 'ubv,R', 'ubv,I']): + + self.evo_model = evo_model + self.atm_func = atm_func + self.wd_atm_func = wd_atm_func + self.wave_range = wave_range + self.distance = distance + self.red_law = red_law + self.AKs = AKs + if hasattr(evo_model, 'external_evol') == False: + raise Exception('The specified evolution model does NOT have external evolution. Use IsochronePhot() instead.') + elif self.evo_model.external_evol == False: + raise Exception('The specified evolution model does NOT have external evolution. Use IsochronePhot() instead.') + + self.external_evol = self.evo_model.external_evol + + self.metallicity = metallicity + self.logAge = logAge + + # Make the iso_dir, if it doesn't already exist + if not os.path.exists(atm_grid_dir): + os.makedirs(atm_grid_dir) + + # Make and input/output file name for the stored photometry. + # For solar metallicity case, allow for legacy isochrones (which didn't have + # metallicity tag since they were all solar metallicity) to be read + # properly + if metallicity == 0.0: + save_file_fmt = '{0}/atm_{1:4.2f}_{2:4s}_p00.fits' + self.save_file = save_file_fmt.format(atm_grid_dir, AKs, str(distance).zfill(5)) + + save_file_legacy = '{0}/atm_{1:4.2f}_{2:4s}.fits' + self.save_file_legacy = save_file_legacy.format(atm_grid_dir, AKs, str(distance).zfill(5)) + else: + # Set metallicity flag + if metallicity < 0: + metal_pre = 'm' + else: + metal_pre = 'p' + metal_flag = int(abs(metallicity)*10) + + save_file_fmt = '{0}/atm_{1:4.2f}_{2:4s}_{3}{4:2s}.fits' + self.save_file = save_file_fmt.format(atm_grid_dir, AKs, str(distance).zfill(5), metal_pre, str(metal_flag).zfill(2)) + self.save_file_legacy = save_file_fmt.format(atm_grid_dir, AKs, str(distance).zfill(5), metal_pre, str(metal_flag).zfill(2)) + + # Expected filters + self.filters = filters + + # Recalculate atmosphere grid if save_file doesn't exist or recomp == True + new_file_exists = check_save_file(self.save_file, evo_model, atm_func, red_law) + legacy_file_exists = ( + self.save_file_legacy != self.save_file and + check_save_file(self.save_file_legacy, evo_model, atm_func, red_law) + ) + file_exists = new_file_exists or legacy_file_exists + + if (not file_exists) | (recomp==True): + self.recalc = True + + c = constants + + t1 = time.time() + + # Assert that the wavelength ranges are within the limits of the + # VEGA model (0.1 - 10 microns) + try: + assert wave_range[0] > 1000 + assert wave_range[1] < 100000 + except: + print('Desired wavelength range invalid. Limit to 1000 - 10000 A') + return + + # The points that will be interpolated over are the grid + # of the ATMOSPHERE MODEL ONLY + # Takes the atm_func and adds the word "_grid" after it to + # call the grid version of it + module = sys.modules[atm_func.__module__] + grid_func = getattr(module, atm_func.__name__ + "_grid") + teff_arr, z_arr, logg_arr = grid_func(rebin=rebin) + + + tab = Table([teff_arr, logg_arr, z_arr], + names=['Teff', 'logg', 'metallicity']) + + + # Initialize output for stellar spectra + self.spec_list = [] + + # Loop through radii too! + # Do WD and stars separately + # For each temperature extract the synthetic photometry. + for ii in range(len(teff_arr)): + # Loop is currently taking about 0.11 s per iteration + gravity = logg_arr[ii] + T = teff_arr[ii] + metallicity = z_arr[ii] + R = float(1*units.Rsun.to('pc')) + + # Get the atmosphere model now. Wavelength is in Angstroms + # This is the time-intensive call... everything else is negligable. + star = atm_func(temperature=T, gravity=gravity, metallicity=metallicity, + rebin=rebin) + + # Trim wavelength range down to JHKL range (0.5 - 5.2 microns) + star = spectrum.trimSpectrum(star, wave_range[0], wave_range[1]) + + # Convert into flux observed at Earth (unreddened) + star *= (R / distance)**2 # in erg s^-1 cm^-2 A^-1 + + # Redden the spectrum. This doesn't take much time at all. + red = red_law.reddening(AKs).resample(star.wave) + star *= red + + # Save the final spectrum to our spec_list for later use. + self.spec_list.append(star) + + # Append all the meta data to the summary table. + tab.meta['REDLAW'] = red_law.name + tab.meta['ATMFUNC'] = atm_func.__name__ + tab.meta['EVOMODEL'] = type(evo_model).__name__ + tab.meta['EVOMODELVERSION'] = evo_model.model_version_name + tab.meta['AKS'] = AKs + tab.meta['DISTANCE'] = distance + tab.meta['WAVEMIN'] = wave_range[0] + tab.meta['WAVEMAX'] = wave_range[1] + + self.points = tab + + t2 = time.time() + print( 'Atmosphere grid generation took {0:f} s.'.format(t2-t1)) + + self.verbose = True + + # Make photometry + self.make_photometry(rebin=rebin, vega=vega) + else: + self.recalc = False + if new_file_exists: + self.points = Table.read(self.save_file) + else: + self.points = Table.read(self.save_file_legacy) + # Add some error checking. + + # Next: do we have all the filters we need? + comp_filters = [] + for ii in self.filters: + col_name = 'm_' + get_filter_col_name(ii) + if col_name not in self.points.keys(): + comp_filters.append(ii) + + # Compute additional filters if needed + if len(comp_filters)>0: + self.verbose = True + print('Missing photometry for',len(comp_filters),'filter - recomputing these columns:',comp_filters) + + # The points that will be interpolated over are the grid + # of the ATMOSPHERE MODEL ONLY + # Takes the atm_func and adds the word "_grid" after it to + # call the grid version of it + module = sys.modules[atm_func.__module__] + grid_func = getattr(module, atm_func.__name__ + "_grid") + teff_arr, z_arr, logg_arr = grid_func(rebin=rebin) + + print('Loading stellar spectra') + # Initialize output for stellar spectra + self.spec_list = [] + # For each isochrone point, extract the synthetic photometry. + for ii in range(len(teff_arr)): + gravity = logg_arr[ii] + T = teff_arr[ii] + metallicity = z_arr[ii] + R = float(1*units.Rsun.to('pc')) + + # Get the atmosphere model now. Wavelength is in Angstroms + # This is the time-intensive call... everything else is negligable. + star = atm_func(temperature=T, gravity=gravity, metallicity=metallicity, + rebin=rebin) + # Trim wavelength range down to appropriate range + star = spectrum.trimSpectrum(star, wave_range[0], wave_range[1]) + # Convert into flux observed at Earth (unreddened) + star *= (R / self.points.meta["DISTANCE"])**2 # in erg s^-1 cm^-2 A^-1 + # Redden the spectrum. This doesn't take much time at all. + red = red_law.reddening(AKs).resample(star.wave) + star *= red + # Save the final spectrum to our spec_list for later use. + self.spec_list.append(star) + + self.make_photometry(rebin=rebin, vega=vega, comp_filters=comp_filters) + + + + return + + def make_photometry(self, rebin=True, vega=vega, comp_filters=None): + """ + Make synthetic photometry for the specified filters. This function + udpates the self.points table to include new columns with the + photometry. + + """ + startTime = time.time() + + meta = self.points.meta + + print( 'Making photometry for atmosphere grid: AKs = %.2f dist = %d' % \ + (meta['AKS'], meta['DISTANCE'])) + print( ' Starting at: ', datetime.datetime.now(), ' Usually takes ~5 minutes') + + npoints = len(self.points) + verbose_fmt = 'M = {0:7.3f} Msun T = {1:5.0f} K m_{2:s} = {3:4.2f}' + + #Calculate all filters, or select filters + if comp_filters is None: + comp_filters = self.filters + + # Loop through the filters, get filter info, make photometry for + # all stars in this filter. + for ii in comp_filters: + prt_fmt = 'Starting filter: {0:s} Elapsed time: {1:.2f} seconds' + print( prt_fmt.format(ii, time.time() - startTime)) + + filt = get_filter_info(ii, rebin=rebin, vega=vega) + filt_name = get_filter_col_name(ii) + + # Make the column to hold magnitudes in this filter. Add to points table. + col_name = 'm_' + filt_name + mag_col = Column(np.zeros(npoints, dtype=float), name=col_name) + self.points.add_column(mag_col) + + # Loop through each star in the isochrone and do the filter integration + print('Starting synthetic photometry') + for ss in range(npoints): + star = self.spec_list[ss] # These are already extincted, observed spectra. + star_mag = mag_in_filter(star, filt) + + self.points[col_name][ss] = star_mag + + if (self.verbose and (ss % 100) == 0): + print( verbose_fmt.format(self.points['Teff'][ss], self.points['logg'][ss], + filt_name, star_mag)) + + endTime = time.time() + print( ' Time taken: {0:.2f} seconds'.format(endTime - startTime)) + + if self.save_file != None: + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + self.points.write(self.save_file, overwrite=True) + + return + + def plot_mass_magnitude(self, mag, savefile=None): + """ + This function is not possible in this Isochrone class + """ + raise Exception('This function is not possible in this Isochrone class') + + return + #===================================================# # Iso table: same as IsochronePhot object, but doesn't do reddening application # or photometry automatically. These are separate functions on the object. diff --git a/spisea/tests/test_imf.py b/spisea/tests/test_imf.py index 969e550d..943dc0a1 100755 --- a/spisea/tests/test_imf.py +++ b/spisea/tests/test_imf.py @@ -19,8 +19,8 @@ def test_generate_cluster(): mass, isMulti, compMass, sysMass = my_imf.generate_cluster(M_cl) # Make sure that the total mass is always within the expected - # range of the requested mass. - assert np.abs(M_cl - sysMass.sum()) < 120.0 + # range of the requested mass (2%). + assert np.abs(M_cl - sysMass.sum()) < M_cl*0.02 # Check that enough companions were generated. # Should be greater than 25% of the stars with companions. diff --git a/spisea/tests/test_models.py b/spisea/tests/test_models.py index e527707d..af73c2c3 100644 --- a/spisea/tests/test_models.py +++ b/spisea/tests/test_models.py @@ -91,6 +91,82 @@ def test_synthpop_MIST_extension(): return +def test_COSMIC_init(): + """ + Test the COSMIC external evolution model constructor: default flags, + default BSEDict, and that user-supplied options are stored. + """ + # Default construction + evo = evolution.COSMIC() + assert evo.external_evol is True + assert evo.z_solar == 0.02 + assert evo.model_version_name == 'COSMIC' + assert evo.keep_disrupted_companions is True + assert evo.keep_COSMIC_tables is False + + # Default BSEDict should be a populated dictionary of BSE parameters + assert isinstance(evo.BSEDict, dict) + assert len(evo.BSEDict) > 0 + + # User-supplied options should be stored + custom_dict = {'windflag': 3, 'neta': 0.5} + evo2 = evolution.COSMIC(BSEDict=custom_dict, keep_disrupted_companions=False, + keep_COSMIC_tables=True) + assert evo2.BSEDict == custom_dict + assert evo2.keep_disrupted_companions is False + assert evo2.keep_COSMIC_tables is True + + return + +def test_COSMIC_calc_logg(): + """ + Test the COSMIC.calc_logg helper. For the Sun (M=1 Msun, R=1 Rsun) + the surface gravity should be logg ~ 4.438 (cgs). + """ + evo = evolution.COSMIC() + + # Scalar solar value + assert np.isclose(evo.calc_logg(1.0, 1.0), 4.438, atol=0.01) + + # Array input should be handled element-wise + masses = np.array([1.0, 2.0]) + radii = np.array([1.0, 2.0]) + logg = evo.calc_logg(masses, radii) + assert np.isclose(logg[0], 4.438, atol=0.01) + # logg scales as log10(M/R^2); doubling both M and R lowers logg by log10(2) + assert np.isclose(logg[0] - logg[1], np.log10(2.0), atol=0.01) + + return + +def test_COSMIC_get_kick_differential(): + """ + Test the COSMIC.get_kick_differential helper. The transformation is a + pure rotation (Rz(theta) * Rx(phi)) of the kick vector, so it must + preserve the vector magnitude. A zero kick must map to a zero kick. + """ + evo = evolution.COSMIC() + + # Zero kick in -> zero kick out + zeros = np.zeros(3) + phase = np.array([0.3, 1.1, 2.0]) + incl = np.array([0.5, 1.5, 2.5]) + kd_zero = evo.get_kick_differential(zeros, zeros, zeros, phase=phase, inclination=incl) + assert np.allclose(kd_zero.d_x.value, 0.0) + assert np.allclose(kd_zero.d_y.value, 0.0) + assert np.allclose(kd_zero.d_z.value, 0.0) + + # Magnitude is preserved under the rotation + vx = np.array([10.0, -5.0, 3.0]) + vy = np.array([2.0, 7.0, -1.0]) + vz = np.array([-4.0, 1.0, 8.0]) + kd = evo.get_kick_differential(vx, vy, vz, phase=phase, inclination=incl) + + mag_in = np.sqrt(vx**2 + vy**2 + vz**2) + mag_out = np.sqrt(kd.d_x.value**2 + kd.d_y.value**2 + kd.d_z.value**2) + np.testing.assert_allclose(mag_out, mag_in, rtol=1e-10) + + return + def test_atmosphere_models(): """ Test the rebinned atmosphere models used for synthetic photometry diff --git a/spisea/tests/test_synthetic.py b/spisea/tests/test_synthetic.py index fb1a15cb..9a304fb5 100755 --- a/spisea/tests/test_synthetic.py +++ b/spisea/tests/test_synthetic.py @@ -2,6 +2,9 @@ import pdb import time import spisea +import pytest +import warnings +import importlib import numpy as np import pylab as plt from astropy.table import Table @@ -941,6 +944,160 @@ def test_compact_object_companions(): assert (len(nan_lum_companions) == 0) | (all(np.isnan(nan_lum_companions['phase'])) == False) +def _require_cosmic(): + """ + Skip the calling test if the optional `cosmic` package is not installed, + emitting a warning so the skip is visible (rather than silent). + """ + if importlib.util.find_spec('cosmic') is None: + msg = ('COSMIC integration test skipped: the optional `cosmic` package ' + 'is not installed. Run in an environment with COSMIC (e.g. ' + '`astro_cosmic`) to exercise these tests.') + warnings.warn(msg) + pytest.skip(msg) + + return + +def test_COSMIC_evolve(): + """ + Test the COSMIC external evolution model's evolve() method directly on a + small, hand-built set of star systems and companions. Uses a young, low-mass + population so no compact remnants/disruptions occur, keeping the run fast + and avoiding the merger/disruption branches. + + Skipped (with a warning) if the optional `cosmic` package is not installed. + """ + _require_cosmic() + + from astropy.table import Table + + # Build a minimal star_systems table: 2 binaries + 1 single + star_systems = Table() + star_systems['mass'] = np.array([1.0, 0.9, 0.5]) + star_systems['isMultiple'] = np.array([True, True, False]) + star_systems['N_companions'] = np.array([1, 1, 0]) + star_systems['systemMass'] = np.array([1.5, 1.3, 0.5]) + + # Companions for the first two systems + companions = Table() + companions['system_idx'] = np.array([0, 1]) + companions['mass'] = np.array([0.5, 0.4]) + companions['log_a'] = np.array([1.0, 1.5]) # log10(AU) + companions['e'] = np.array([0.1, 0.2]) + companions['i'] = np.array([30.0, 60.0]) # degrees + companions['Omega'] = np.array([0.0, 0.0]) + companions['omega'] = np.array([0.0, 0.0]) + + evo = evolution.COSMIC(keep_disrupted_companions=False, keep_COSMIC_tables=True) + ss, comp = evo.evolve(star_systems, companions, logAge=8.0, metallicity=0.0) + + # Check that evolve() populated the expected output columns + expected_cols = ['mass_current', 'Teff', 'L', 'logg', 'phase', + 'kick', 'kick_x', 'kick_y', 'kick_z'] + for col in expected_cols: + assert col in ss.colnames, 'star_systems missing column {0}'.format(col) + assert col in comp.colnames, 'companions missing column {0}'.format(col) + + # Core bookkeeping invariant: companion count must match companion table length + assert np.sum(ss['N_companions']) == len(comp) + + # Scalar kick must be the magnitude of the kick vector components + np.testing.assert_allclose( + ss['kick'], np.sqrt(ss['kick_x']**2 + ss['kick_y']**2 + ss['kick_z']**2)) + np.testing.assert_allclose( + comp['kick'], np.sqrt(comp['kick_x']**2 + comp['kick_y']**2 + comp['kick_z']**2)) + + # Young, low-mass stars should remain stellar (no compact remnants here). + # Compact-object phase codes are 101 (WD), 102 (NS), 103 (BH). + assert np.all(ss['phase'] < 100) + assert np.all(comp['phase'] < 100) + + # keep_COSMIC_tables=True should store the raw COSMIC output tables + for attr in ['bpp', 'bcm', 'initC', 'kick_info']: + assert hasattr(evo, attr), 'COSMIC model missing table {0}'.format(attr) + + return + +def test_COSMIC_ResolvedCluster(): + """ + Test the full COSMIC cluster pipeline, mirroring the Cluster_w_COSMIC + tutorial: build an IsochronePhotExternalEvolution, then a ResolvedCluster + with binary multiplicity (CSF_max=1, companion_max=True), and check the + output tables. + + Skipped (with a warning) if the optional `cosmic` package is not installed. + """ + _require_cosmic() + + # Cluster/isochrone parameters (kept small/cheap) + logAge = 9.0 + AKs = 0.0 + distance = 4000 + metallicity = 0.0 + cluster_mass = 10**3. + mass_sampling = 10 + atm_grid_dir = f'{spisea_path}/tests/atm_cosmic' + + filt_list = ['ubv,V'] + + # External evolution model (keep tables so we can verify them) + evo = evolution.COSMIC(keep_COSMIC_tables=True) + atm_func = atmospheres.get_merged_atmosphere_w_bb_supplement + red_law = reddening.RedLawCardelli(3.1) + + iso = syn.IsochronePhotExternalEvolution( + logAge, + AKs, + distance, + metallicity=metallicity, + evo_model=evo, + atm_func=atm_func, + red_law=red_law, + filters=filt_list, + atm_grid_dir=atm_grid_dir, + mass_sampling=mass_sampling, + recomp=False + ) + + # COSMIC only supports binaries: resolved multiplicity, no higher-order systems + clust_multiplicity = multiplicity.MultiplicityResolvedDK(CSF_max=1, companion_max=True) + my_imf = imf.Kroupa_2001(multiplicity=clust_multiplicity) + + cluster = syn.ResolvedCluster(iso, my_imf, cluster_mass) + star_systems = cluster.star_systems + companions = cluster.companions + + # Basic sanity: stars and companions were produced + assert len(star_systems) > 0 + assert len(companions) > 0 + + # Core bookkeeping invariant + assert np.sum(star_systems['N_companions']) == len(companions) + + # Kick columns present and self-consistent on both tables + for tab in (star_systems, companions): + for col in ['kick', 'kick_x', 'kick_y', 'kick_z']: + assert col in tab.colnames + np.testing.assert_allclose( + tab['kick'], np.sqrt(tab['kick_x']**2 + tab['kick_y']**2 + tab['kick_z']**2)) + + # Synthetic photometry column should exist + assert 'm_ubv_V' in star_systems.colnames + + # All phase codes should be in the allowed set: + # 0-9 stellar phases, 101 (WD), 102 (NS), 103 (BH) + allowed_phases = set(range(0, 10)) | {101, 102, 103} + ss_phases = set(np.unique(star_systems['phase']).astype(int).tolist()) + comp_phases = set(np.unique(companions['phase']).astype(int).tolist()) + assert ss_phases.issubset(allowed_phases), 'Unexpected star phases: {0}'.format(ss_phases - allowed_phases) + assert comp_phases.issubset(allowed_phases), 'Unexpected companion phases: {0}'.format(comp_phases - allowed_phases) + + # keep_COSMIC_tables=True should expose the raw COSMIC tables on the evo model + for attr in ['bpp', 'bcm', 'initC', 'kick_info']: + assert hasattr(iso.evo_model, attr), 'COSMIC model missing table {0}'.format(attr) + + return + #=================================# # Additional timing functions #=================================#