├── pyradex ├── tests │ ├── __init__.py │ ├── data │ │ ├── example.inp │ │ └── example.out │ ├── test_radex_install.py │ ├── setup_package_data.py │ ├── test_synthspec.py │ ├── test_grid.py │ ├── test_radex_myradex_consistency.py │ ├── test_fjdu.py │ └── test_radex.py ├── version.py ├── radex │ └── __init__.py ├── fjdu │ ├── __init__.py │ └── core.py ├── __init__.py ├── read_radex.py ├── grid_wrapper.py ├── utils.py ├── synthspec.py ├── despotic_interface.py └── base_class.py ├── requirements.txt ├── pip-requirements ├── setup.cfg ├── .gitignore ├── .gitmodules ├── MANIFEST.in ├── examples ├── simple_co_column.inp ├── simple_co.inp ├── utils.py ├── simple_co.py ├── simple_co_column.py ├── ch3cn_110_synthspec.py ├── ph2co_grid_computation.py ├── synthspec_ch3cn.py ├── interactive_setup_mm.py ├── compare_co_radex_despotic.py ├── ph2co_grids.py ├── fjdu_vs_radex.py ├── h2cs_thermometer.py ├── h2co_grids.py ├── timing.py ├── ph2co_required_sn.py ├── oh2co_density_grid.py └── ph2co_grid_computation_mm.py ├── CHANGES.rst ├── docs ├── index.rst └── notes.rst ├── LICENSE.rst ├── INSTALL.rst ├── setup.py ├── .travis.yml ├── install_radex.py └── README.rst /pyradex/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astropy>=1.2 2 | -------------------------------------------------------------------------------- /pyradex/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.4.2dev" 2 | -------------------------------------------------------------------------------- /pyradex/radex/__init__.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | from . import radex 3 | -------------------------------------------------------------------------------- /pip-requirements: -------------------------------------------------------------------------------- 1 | astropy>=0.4.1 2 | astroquery>=0.2 3 | requests>=2.4.1 4 | -------------------------------------------------------------------------------- /pyradex/fjdu/__init__.py: -------------------------------------------------------------------------------- 1 | from . import core 2 | from .core import Fjdu 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | python_files=test_*.py 3 | addopts = --ignore=setup.py 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *pyc 2 | *__pycache__* 3 | *pdf 4 | *png 5 | *inp 6 | *log 7 | *dat 8 | ipython*py 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "myRadex"] 2 | path = myRadex 3 | url = https://github.com/keflavich/myRadex.git 4 | -------------------------------------------------------------------------------- /pyradex/tests/data/example.inp: -------------------------------------------------------------------------------- 1 | hco+.dat 2 | example.out 3 | 50 500 4 | 20.0 5 | 1 6 | H2 7 | 1e4 8 | 2.73 9 | 1e13 10 | 1.0 11 | 0 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include INSTALL.rst 3 | include README.rst 4 | include CHANGES.rst 5 | include pip-requirements 6 | include install_radex.py 7 | -------------------------------------------------------------------------------- /pyradex/tests/test_radex_install.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def test_radex_install(): 4 | from pyradex.radex import radex 5 | 6 | assert radex.setup.radat.dtype == np.dtype('S1') 7 | -------------------------------------------------------------------------------- /pyradex/tests/setup_package_data.py: -------------------------------------------------------------------------------- 1 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 2 | import os 3 | 4 | def get_package_data(): 5 | paths_test = [os.path.join('data', '*.out')] 6 | return {'pyradex.tests': paths_test} 7 | -------------------------------------------------------------------------------- /pyradex/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | 3 | from .core import pyradex,write_input,parse_outfile,call_radex,Radex 4 | 5 | from . import utils 6 | from . import despotic_interface 7 | from . import radex 8 | from . import synthspec 9 | -------------------------------------------------------------------------------- /pyradex/tests/test_synthspec.py: -------------------------------------------------------------------------------- 1 | from ..core import Radex 2 | from .. import synthspec 3 | import numpy as np 4 | import astropy.units as u 5 | 6 | def test_synthspec(): 7 | R = Radex(column=1e15, temperature=20, density=1e3) 8 | R.run_radex() 9 | wcs = np.linspace(103.0, 103.1, 1000)*u.GHz 10 | S = synthspec.SyntheticSpectrum.from_RADEX(wcs, R, linewidth=10*u.km/u.s) 11 | -------------------------------------------------------------------------------- /examples/simple_co_column.inp: -------------------------------------------------------------------------------- 1 | co.dat 2 | simple_co_column.out 3 | 100 1000 4 | 20.0 5 | 1 6 | H2 7 | 1e3 8 | 2.73 9 | 1e13 10 | 1.0 11 | 1 12 | co.dat 13 | simple_co_column.out 14 | 100 1000 15 | 20.0 16 | 1 17 | H2 18 | 1e3 19 | 2.73 20 | 1e14 21 | 1.0 22 | 1 23 | co.dat 24 | simple_co_column.out 25 | 100 1000 26 | 20.0 27 | 1 28 | H2 29 | 1e3 30 | 2.73 31 | 1e15 32 | 1.0 33 | 1 34 | co.dat 35 | simple_co_column.out 36 | 100 1000 37 | 20.0 38 | 1 39 | H2 40 | 1e3 41 | 2.73 42 | 1e16 43 | 1.0 44 | 1 45 | co.dat 46 | simple_co_column.out 47 | 100 1000 48 | 20.0 49 | 1 50 | H2 51 | 1e3 52 | 2.73 53 | 1e17 54 | 1.0 55 | 0 56 | -------------------------------------------------------------------------------- /examples/simple_co.inp: -------------------------------------------------------------------------------- 1 | co.dat 2 | example.out 3 | 100 1000 4 | 5.0 5 | 1 6 | H2 7 | 1e3 8 | 2.73 9 | 1e16 10 | 1.0 11 | 1 12 | co.dat 13 | example.out 14 | 100 1000 15 | 10.0 16 | 1 17 | H2 18 | 1e3 19 | 2.73 20 | 1e16 21 | 1.0 22 | 1 23 | co.dat 24 | example.out 25 | 100 1000 26 | 20.0 27 | 1 28 | H2 29 | 1e3 30 | 2.73 31 | 1e16 32 | 1.0 33 | 1 34 | co.dat 35 | example.out 36 | 100 1000 37 | 30.0 38 | 1 39 | H2 40 | 1e3 41 | 2.73 42 | 1e16 43 | 1.0 44 | 1 45 | co.dat 46 | example.out 47 | 100 1000 48 | 40.0 49 | 1 50 | H2 51 | 1e3 52 | 2.73 53 | 1e16 54 | 1.0 55 | 1 56 | co.dat 57 | example.out 58 | 100 1000 59 | 50.0 60 | 1 61 | H2 62 | 1e3 63 | 2.73 64 | 1e16 65 | 1.0 66 | 1 67 | co.dat 68 | example.out 69 | 100 1000 70 | 60.0 71 | 1 72 | H2 73 | 1e3 74 | 2.73 75 | 1e16 76 | 1.0 77 | 0 78 | -------------------------------------------------------------------------------- /pyradex/tests/test_grid.py: -------------------------------------------------------------------------------- 1 | from .. import Radex 2 | import numpy as np 3 | 4 | def buildgrid(densities=np.logspace(0,8,20), abundance=10**-8.5, fortho=1e-3, **kwargs): 5 | 6 | tau = np.zeros_like(densities) 7 | 8 | R = Radex(species='co', abundance=abundance, collider_densities={'oH2':10,'pH2':10}) 9 | for jj,dd in enumerate(densities): 10 | R.density = {'oH2':dd*fortho,'pH2':dd*(1-fortho)} 11 | R.abundance = abundance # reset column to the appropriate value 12 | R.run_radex(**kwargs) 13 | tau[jj] = R.tau[0] 14 | 15 | return tau 16 | 17 | def try_variants(): 18 | 19 | taud = {} 20 | for reuse_last in (True,False): 21 | for reload_molfile in (True,False): 22 | s = ('T' if reuse_last else 'F') + ('T' if reload_molfile else 'F') 23 | taud[s] = buildgrid(reuse_last=reuse_last, reload_molfile=reload_molfile) 24 | 25 | return taud 26 | -------------------------------------------------------------------------------- /examples/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Temporary, until it's merged in astropy: https://github.com/astropy/astropy/pull/2793 3 | """ 4 | def in_ipynb_kernel(): 5 | """ 6 | Determine whether the current code is being executed from within an IPython 7 | notebook kernel. If ``True``, the code may be executed from a notebook or 8 | from a console connected to a notebook kernel, but (we believe) it's not 9 | being executed in a console that was initialized from the command line. 10 | """ 11 | try: 12 | cfg = get_ipython().config 13 | app = cfg['IPKernelApp'] 14 | # ipython 1.0 console has no 'parent_appname', 15 | # but ipynb does 16 | if ('parent_appname' in app and 17 | app['parent_appname'] == 'ipython-notebook'): 18 | return True 19 | else: 20 | return False 21 | except NameError: 22 | # NameError will occur if this is called from python (not ipython) 23 | return False 24 | -------------------------------------------------------------------------------- /examples/simple_co.py: -------------------------------------------------------------------------------- 1 | import pyradex 2 | import pylab as pl 3 | 4 | R = pyradex.Radex(column=1e16) 5 | R.maxiter=1000 6 | #print R.radex.impex.molfile,R.molpath 7 | 8 | for temperature in [5,10,20,30,40,50,60]: 9 | R.temperature = temperature 10 | 11 | R.run_radex() 12 | 13 | pl.figure(1) 14 | pl.plot(R.level_population, label="T=%s" % R.temperature._repr_latex_()) 15 | 16 | pl.figure(2) 17 | pl.plot(R.frequency, R.tau, label="T=%s" % R.temperature._repr_latex_()) 18 | 19 | pl.figure(3) 20 | pl.plot(R.frequency, R.tex, label="T=%s" % R.temperature._repr_latex_()) 21 | 22 | f1 = pl.figure(1) 23 | ax1 = f1.gca() 24 | ax1.set_xlim(0,10) 25 | ax1.set_xlabel("Energy Level") 26 | ax1.set_ylabel("Population") 27 | 28 | f2 = pl.figure(2) 29 | ax2 = f2.gca() 30 | ax2.set_xlabel("Frequency") 31 | ax2.set_ylabel("Optical Depth") 32 | ax2.set_xlim(0,1000) 33 | 34 | f3 = pl.figure(3) 35 | ax3 = f3.gca() 36 | ax3.set_xlabel("Frequency") 37 | ax3.set_ylabel("Excitation Temperature") 38 | ax3.axis([0,1000,0,65]) 39 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | 0.4.0 - November 1, 2014: Add Fujun Du's "myradex" with an identical interface 5 | to RADEX. Factored out common functions to base_class.py 6 | 7 | 0.3.1 - October 28, 2014: allow 'validate_colliders' to be disabled so that 8 | fast grids can be run. Fast grids require validate_colliders=False, 9 | reload_molfile=False. 10 | - Major refactor of quantity/units use: factor of ~10 speedup for default 11 | R.__call__ approach. 12 | 13 | 0.3.0 - August 28, 2014: MAJOR internal refactor and API change. `astropy` 14 | is now required, and there is no longer a 'total h2 column' concept 15 | associated with the RADEX code. For compatibility with DESPOTIC, there 16 | will need to be an additional interface layer. 17 | Changed default species too, since `hco+` doesn't exist on the LAMDA 18 | servers. 19 | 20 | 0.2.2 - August 27, 2014: Minor bugfixes including update of specutils version 21 | inclusion 22 | 23 | 0.2.1 - May 29, 2014 - bugfix release. First to include CHANGES. Intended to 24 | work when pip-installing 25 | 26 | 0.2 - first python-wrapped version? 27 | -------------------------------------------------------------------------------- /examples/simple_co_column.py: -------------------------------------------------------------------------------- 1 | import pyradex 2 | import pylab as pl 3 | import numpy as np 4 | 5 | R = pyradex.Radex(column=1e16,temperature=20) 6 | R.maxiter=1000 7 | #print R.radex.impex.molfile,R.molpath 8 | 9 | for column in np.linspace(1e14,1e17,10): 10 | R.column = column 11 | 12 | #R.debug = 1 13 | 14 | R.run_radex(reuse_last=True) 15 | 16 | pl.figure(1) 17 | pl.plot(R.level_population, label="c=%e" % column) 18 | 19 | pl.figure(2) 20 | pl.plot(R.frequency, R.tau, label="c=%e" % column) 21 | 22 | pl.figure(3) 23 | pl.plot(R.frequency, R.tex, label="c=%e" % column) 24 | 25 | f1 = pl.figure(1) 26 | ax1 = f1.gca() 27 | ax1.set_xlim(0,10) 28 | ax1.set_xlabel("Energy Level") 29 | ax1.set_ylabel("Population") 30 | 31 | f2 = pl.figure(2) 32 | ax2 = f2.gca() 33 | ax2.set_xlabel("Frequency") 34 | ax2.set_ylabel("Optical Depth") 35 | ax2.set_xlim(0,1000) 36 | 37 | f3 = pl.figure(3) 38 | ax3 = f3.gca() 39 | ax3.set_xlabel("Frequency") 40 | ax3.set_ylabel("Excitation Temperature") 41 | ax3.axis([0,1000,0,65]) 42 | 43 | print("Same as above, but not starting from last position.") 44 | for column in np.linspace(1e14,1e17,10): 45 | R.column = column 46 | 47 | R.run_radex(reuse_last=False) 48 | -------------------------------------------------------------------------------- /pyradex/tests/data/example.out: -------------------------------------------------------------------------------- 1 | * Radex version : 20nov08 2 | * Geometry : Uniform sphere 3 | * Molecular data file : /Users/adam/repos/Radex/data/hco+.dat 4 | * T(kin) [K]: 20.000 5 | * Density of H2 [cm-3]: 1.000E+04 6 | * T(background) [K]: 2.730 7 | * Column density [cm-2]: 1.000E+13 8 | * Line width [km/s]: 1.000 9 | Calculation finished in 67 iterations 10 | LINE E_UP FREQ WAVEL T_EX TAU T_R POP POP FLUX FLUX 11 | (K) (GHz) (um) (K) (K) UP LOW (K*km/s) (erg/cm2/s) 12 | 1 -- 0 4.3 89.1884 3361.3393 4.505 4.686E+00 1.557E+00 4.897E-01 4.221E-01 1.657E+00 1.514E-08 13 | 2 -- 1 12.8 178.3748 1680.6883 3.769 5.300E+00 5.927E-01 8.419E-02 4.897E-01 6.309E-01 4.612E-08 14 | 3 -- 2 25.7 267.5573 1120.4795 3.724 8.856E-01 1.789E-01 3.750E-03 8.419E-02 1.905E-01 4.698E-08 15 | 4 -- 3 42.8 356.7338 840.3814 6.033 3.652E-02 3.703E-02 2.823E-04 3.750E-03 3.941E-02 2.304E-08 16 | 5 -- 4 64.2 445.9024 672.3275 9.702 2.527E-03 6.664E-03 3.801E-05 2.823E-04 7.094E-03 8.100E-09 17 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | PyRADEX 2 | ======= 3 | 4 | 5 | ``pyradex`` is a python-wrapped version of RADEX_, using f2py to make grid 6 | building *nearly* as fast as the Fortran code, but much more convenient. 7 | 8 | ``pyradex`` also wraps Mark Krumholz's DESPOTIC_ code with an interface 9 | identical to pyradex so that the codes can be directly compared. However, I 10 | have achieved no success yet in matching the code results! I have some 11 | :doc:`notes` about this process, but they are messy and incomplete. 12 | 13 | Using pyradex 14 | ------------- 15 | 16 | pyradex is centered around the `~pyradex.core.Radex` class. To initialize it, a 17 | valid ``.dat`` file must be available in the appropriate `directory `_. 18 | 19 | Then, initialize the `~pyradex.core.Radex` class by giving it initial 20 | conditions and a chemical species: 21 | 22 | >>> import pyradex 23 | >>> rdx = pyradex.Radex(species='hco+', collider_densities={'H2':1e3}, 24 | ... column=1e13) 25 | 26 | To get values from the object, you need to run RADEX: 27 | 28 | >>> rdx.run_radex() 29 | >>> print(rdx.tex) 30 | >>> tbl = rdx.get_table() 31 | 32 | 33 | .. _directory_setup: 34 | 35 | Directory Configuration 36 | ----------------------- 37 | 38 | 39 | .. _RADEX: http://home.strw.leidenuniv.nl/~moldata/radex.html 40 | .. _DESPOTIC: https://sites.google.com/a/ucsc.edu/krumholz/codes/despotic 41 | -------------------------------------------------------------------------------- /pyradex/tests/test_radex_myradex_consistency.py: -------------------------------------------------------------------------------- 1 | from ..core import Radex 2 | from .. import fjdu 3 | import pytest 4 | import os 5 | import distutils.spawn 6 | import numpy as np 7 | from astropy import units as u 8 | from astropy import log 9 | 10 | def data_path(filename): 11 | data_dir = os.path.join(os.path.dirname(__file__), 'data') 12 | return os.path.join(data_dir, filename) 13 | 14 | def test_thin_co(): 15 | density = {'oH2': 100, 16 | 'pH2': 900, 17 | } 18 | RR = Radex(datapath='examples/', species='co', column=1e10, 19 | density=density, temperature=20) 20 | FF = fjdu.Fjdu(datapath='examples/', species='co', 21 | column=1e10, density=density, temperature=20) 22 | 23 | rtbl = RR() 24 | ftbl = FF() 25 | 26 | diff = rtbl['upperlevelpop'] - ftbl['upperlevelpop'] 27 | log.info(diff) 28 | #np.testing.assert_allclose(diff, 0) 29 | 30 | def test_thick_co(): 31 | density = {'oH2': 100, 32 | 'pH2': 900, 33 | } 34 | RR = Radex(datapath='examples/', species='co', column=1e17, 35 | density=density, temperature=20) 36 | FF = fjdu.Fjdu(datapath='examples/', species='co', 37 | column=1e17, density=density, temperature=20) 38 | 39 | rtbl = RR() 40 | ftbl = FF() 41 | 42 | diff = rtbl['upperlevelpop'] - ftbl['upperlevelpop'] 43 | log.info(diff) 44 | #np.testing.assert_allclose(diff, 0) 45 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2015, Adam Ginsburg 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /examples/ch3cn_110_synthspec.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: 3 | Plot the relative intensities of the 3mm CH3CN (methyl cyanide) lines at 110 GHz 4 | """ 5 | import pyradex 6 | import pylab as pl 7 | from astropy import units as u 8 | import numpy as np 9 | import matplotlib as mpl 10 | 11 | R = pyradex.Radex(species='ch3cn',abundance=1e-11,column=None) 12 | 13 | # There are 5 lines of interest in this band 14 | nlines = 6 15 | fluxes = {ii:[] for ii in xrange(nlines)} 16 | 17 | # Temperature range: 20-500 K is allowed (by CH3CN data file) 18 | temperatures = np.linspace(20,500) 19 | 20 | # set up figure 21 | pl.figure(1) 22 | pl.clf() 23 | 24 | for ii,temperature in enumerate(temperatures): 25 | 26 | R.temperature = temperature 27 | R.run_radex() 28 | wcs = np.linspace(110.326, 110.388, 1000)*u.GHz 29 | S = pyradex.synthspec.SyntheticSpectrum.from_RADEX(wcs,R) 30 | 31 | # spectral colors 32 | color = mpl.cm.spectral(float(ii)/len(temperatures)) 33 | S.plot(label='%i K' % temperature,color=color) 34 | 35 | for ii in xrange(nlines): 36 | fluxes[ii].append(S.T_B[ii].value) 37 | 38 | pl.savefig("CH3CN_6-5_synthetic_spectra.pdf",bbox_inches='tight') 39 | 40 | linenames = {ii:S.table[ii]['upperlevel']+" - "+S.table[ii]['lowerlevel'] for ii in xrange(nlines)} 41 | 42 | pl.figure(2) 43 | pl.clf() 44 | pl.subplot(1,2,1) 45 | for ii in xrange(nlines): 46 | pl.plot(temperatures,np.array(fluxes[ii]),label=linenames[ii]) 47 | 48 | # Line #4 is the "reference line" at lowest energy 49 | pl.subplot(1,2,2) 50 | for ii in xrange(nlines): 51 | pl.plot(temperatures,np.array(fluxes[ii])/np.array(fluxes[4]),label=linenames[ii]) 52 | 53 | pl.savefig("CH3CN_6-5_flux_ratios.pdf",bbox_inches='tight') 54 | 55 | pl.figure(3) 56 | pl.clf() 57 | for ii in xrange(nlines): 58 | pl.plot(temperatures,np.array(fluxes[4])/np.array(fluxes[ii]),label=linenames[ii]) 59 | 60 | pl.legend(loc='best') 61 | -------------------------------------------------------------------------------- /examples/ph2co_grid_computation.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create some simple grids for the low-frequency para-H2CO lines 3 | """ 4 | import pyradex 5 | import numpy as np 6 | 7 | ntemp,ndens = 20,20 8 | 9 | temperatures = np.linspace(10,50,ntemp) 10 | densities = np.logspace(2.5,7,ndens) 11 | abundance = 10**-8.5 12 | opr = 0.01 # assume primarily para 13 | fortho = opr/(1+opr) 14 | 15 | taugrid_71M = np.empty([ndens,ntemp]) 16 | texgrid_71M = np.empty([ndens,ntemp]) 17 | fluxgrid_71M = np.empty([ndens,ntemp]) 18 | taugrid_145 = np.empty([ndens,ntemp]) 19 | texgrid_145 = np.empty([ndens,ntemp]) 20 | fluxgrid_145 = np.empty([ndens,ntemp]) 21 | taugrid_355M = np.empty([ndens,ntemp]) 22 | texgrid_355M = np.empty([ndens,ntemp]) 23 | fluxgrid_355M = np.empty([ndens,ntemp]) 24 | columngrid = np.empty([ndens,ntemp]) 25 | 26 | import os 27 | if not os.path.exists('ph2co-h2.dat'): 28 | import urllib 29 | urllib.urlretrieve('http://home.strw.leidenuniv.nl/~moldata/datafiles/ph2co-h2.dat') 30 | 31 | R = pyradex.Radex(species='ph2co-h2', abundance=abundance) 32 | R.run_radex() 33 | 34 | # get the table so we can look at the frequency grid 35 | table = R.get_table() 36 | 37 | # Target frequencies: 38 | table[np.array([6,1,11])].pprint() 39 | 40 | for ii,tt in enumerate(temperatures): 41 | R.temperature = tt 42 | for jj,dd in enumerate(densities): 43 | R.density = {'oH2':dd*fortho,'pH2':dd*(1-fortho)} 44 | R.abundance = abundance # reset column to the appropriate value 45 | R.run_radex(reuse_last=False, reload_molfile=True) 46 | 47 | TI = R.source_line_surfbrightness 48 | taugrid_71M[jj,ii] = R.tau[6] 49 | texgrid_71M[jj,ii] = R.tex[6].value 50 | fluxgrid_71M[jj,ii] = TI[6].value 51 | taugrid_145[jj,ii] = R.tau[1] 52 | texgrid_145[jj,ii] = R.tex[1].value 53 | fluxgrid_145[jj,ii] = TI[1].value 54 | taugrid_355M[jj,ii] = R.tau[11] 55 | texgrid_355M[jj,ii] = R.tex[11].value 56 | fluxgrid_355M[jj,ii] = TI[11].value 57 | columngrid[jj,ii] = R.column.value 58 | -------------------------------------------------------------------------------- /pyradex/tests/test_fjdu.py: -------------------------------------------------------------------------------- 1 | from .. import fjdu 2 | import numpy as np 3 | from astropy import units as u 4 | 5 | def test_simple(): 6 | 7 | FF = fjdu.Fjdu(datapath='examples/', species='co', column=1e15, 8 | density={'pH2':1e3,'oH2':0}, temperature=20) 9 | 10 | assert FF.params['ncol_x_cgs'] == 1e15 11 | np.testing.assert_almost_equal(FF.params['dens_x_cgs'], 1e3) 12 | #np.testing.assert_almost_equal(FF.params['oh2_density_cgs'], 1.7739422658722102) 13 | #np.testing.assert_almost_equal(FF.params['ph2_density_cgs'], 998.22605773412772) 14 | np.testing.assert_almost_equal(FF.params['ph2_density_cgs'], 1e3) 15 | assert FF.params['tkin'] == 20 16 | 17 | tbl = FF() 18 | 19 | #np.testing.assert_almost_equal(tbl[0]['Tex'], 8.69274406690759) 20 | np.testing.assert_almost_equal(tbl[0]['Tex'], 8.6897105103500127) 21 | 22 | def test_mod_params(): 23 | 24 | FF = fjdu.Fjdu(datapath='examples/', species='co', column=1e15, 25 | density={'pH2':1e3,'oH2':0}, temperature=20) 26 | 27 | tbl = FF() 28 | 29 | # Before para/ortho h2 setting, was this 30 | #np.testing.assert_almost_equal(tbl[0]['Tex'], 8.69274406690759) 31 | #np.testing.assert_almost_equal(tbl[0]['Tex'], 8.6935272872891236) 32 | np.testing.assert_almost_equal(tbl[0]['Tex'], 8.6897105103500127) 33 | 34 | FF.column = 1e14 35 | tbl = FF() 36 | 37 | #np.testing.assert_almost_equal(tbl[0]['Tex'], 8.0986662583317646) 38 | #np.testing.assert_almost_equal(tbl[0]['Tex'], 8.0994405565362371) 39 | np.testing.assert_almost_equal(tbl[0]['Tex'], 8.0956672866767292) 40 | 41 | FF.density=1e4 42 | tbl = FF() 43 | np.testing.assert_almost_equal(tbl[0]['Tex'], 25.382518594741391) 44 | 45 | FF.temperature=25 46 | tbl = FF() 47 | np.testing.assert_almost_equal(tbl[0]['Tex'], 37.463006941695028) 48 | 49 | FF.deltav = 5 * u.km/u.s 50 | np.testing.assert_almost_equal(FF.deltav.to(u.km/u.s).value, 5) 51 | tbl = FF() 52 | np.testing.assert_almost_equal(tbl[0]['Tex'], 37.760227295047343) 53 | -------------------------------------------------------------------------------- /examples/synthspec_ch3cn.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example: 3 | Plot the relative intensities of the 3mm CH3CN (methyl cyanide) lines 4 | """ 5 | import pyradex 6 | import pylab as pl 7 | from astropy import units as u 8 | import numpy as np 9 | import matplotlib as mpl 10 | 11 | R = pyradex.Radex(species='ch3cn',abundance=1e-11,column=None) 12 | 13 | # There are 5 lines of interest in this band 14 | nlines = 5 15 | fluxes = {ii:[] for ii in xrange(nlines)} 16 | 17 | # Temperature range: 20-500 K is allowed (by CH3CN data file) 18 | temperatures = np.linspace(20,500,8) 19 | temperatures = [20,50,100,300,500] 20 | 21 | # set up figure 22 | pl.figure(1) 23 | pl.clf() 24 | 25 | for ii,temperature in enumerate(temperatures): 26 | 27 | R.temperature = temperature 28 | R.run_radex() 29 | #wcs = pyradex.synthspec.FrequencyArray(91.95*u.GHz,92*u.GHz,1000) 30 | wcs = np.linspace(91.95,92,1000)*u.GHz 31 | S = pyradex.synthspec.SyntheticSpectrum.from_RADEX(wcs,R,linewidth=5*u.km/u.s)#R.get_table(),'ch3cn') 32 | 33 | # spectral colors 34 | color = mpl.cm.spectral(float(ii)/(len(temperatures))) 35 | S.plot(label='%i K' % temperature, color=color, linewidth=2, alpha=0.9) 36 | 37 | for ii in xrange(nlines): 38 | fluxes[ii].append(S.table[ii]['T_B']) 39 | 40 | pl.legend(loc='best') 41 | pl.savefig("CH3CN_synthetic_spectra.pdf",bbox_inches='tight') 42 | 43 | linenames = {ii:S.table[ii]['upperlevel']+" - "+S.table[ii]['lowerlevel'] for ii in xrange(nlines)} 44 | 45 | pl.figure(2) 46 | pl.clf() 47 | pl.subplot(1,2,1) 48 | for ii in xrange(nlines): 49 | pl.plot(temperatures,fluxes[ii],label=linenames[ii]) 50 | 51 | # Line #4 is the "reference line" at lowest energy 52 | pl.subplot(1,2,2) 53 | for ii in xrange(nlines): 54 | pl.plot(temperatures,np.array(fluxes[ii])/np.array(fluxes[4]),label=linenames[ii]) 55 | 56 | pl.savefig("CH3CN_flux_ratios.pdf",bbox_inches='tight') 57 | 58 | pl.figure(3) 59 | pl.clf() 60 | for ii in xrange(nlines): 61 | pl.plot(temperatures,np.array(fluxes[4])/np.array(fluxes[ii]),label=linenames[ii]) 62 | 63 | pl.legend(loc='best') 64 | -------------------------------------------------------------------------------- /pyradex/read_radex.py: -------------------------------------------------------------------------------- 1 | def tryfloat(x): 2 | try: 3 | return float(x) 4 | except ValueError: 5 | return float(0) 6 | 7 | def read_radex(file,flow,fupp,bw=0.01,debug=False): 8 | """ 9 | less hack-ey way to read radex.out files 10 | """ 11 | linenum = 0 12 | line = file.readline() 13 | linenum+=1 14 | if debug: print (line) 15 | if line == '': 16 | return 0 17 | words = line.split() 18 | if words[1] == '--': 19 | freq = tryfloat(words[4]) 20 | else: 21 | freq = 0 22 | while not(freq*(1-bw) < flow < freq*(1+bw)): 23 | if words[1] == 'T(kin)': 24 | tkin = tryfloat(words[3]) 25 | elif line.find("Density of H2") != -1: 26 | dens = tryfloat(words[5]) 27 | elif line.find("Density of pH2") != -1: 28 | pdens = tryfloat(words[5]) 29 | elif line.find("Density of oH2") != -1: 30 | odens = tryfloat(words[5]) 31 | elif line.find("Column density") != -1: 32 | col = tryfloat(words[4]) 33 | line = file.readline(); linenum+=1 34 | words = line.split() 35 | if words[1] == '--': 36 | freq = tryfloat(words[4]) 37 | TexLow = tryfloat(words[6]) 38 | TauLow = tryfloat(words[7]) 39 | TrotLow = tryfloat(words[8]) 40 | FluxLow = tryfloat(words[11]) 41 | line = file.readline(); linenum+=1 42 | words = line.split() 43 | if words[1] == '--': 44 | freq = tryfloat(words[4]) 45 | while not(freq*(1-bw) < fupp < freq*(1+bw)): 46 | line = file.readline(); linenum+=1 47 | if debug: print (freq,flow,line) 48 | words = line.split() 49 | if words[1] == '--': 50 | freq = tryfloat(words[4]) 51 | TexUpp = tryfloat(words[6]) 52 | TauUpp = tryfloat(words[7]) 53 | TrotUpp = tryfloat(words[8]) 54 | FluxUpp = tryfloat(words[11]) 55 | while len(words) > 0 and words[1] == '--': 56 | line = file.readline(); linenum+=1 57 | words = line.split() 58 | return tkin,dens,col,TexLow,TexUpp,TauLow,TauUpp,TrotLow,TrotUpp,FluxLow,FluxUpp 59 | -------------------------------------------------------------------------------- /examples/interactive_setup_mm.py: -------------------------------------------------------------------------------- 1 | styleargs = {'linewidth': 2, 'alpha': 0.5, 'color':'#5A228B'} 2 | 3 | def setup(tem=5,dens=5,taugrid=taugrid_303,texgrid=texgrid_303): 4 | fig, ((ax1,ax2),(ax3,ax4)) = plt.subplots(2,2, sharex='col', sharey='row', squeeze=True, figsize=(10,7)) 5 | plt.subplots_adjust(hspace=0,wspace=0) 6 | lines1, = ax1.plot(temperatures, taugrid[dens,:], **styleargs) 7 | ax1.set_ylim(-0.2,0.2) 8 | p1, = ax1.plot(temperatures[tem],taugrid[dens,tem], 'o',alpha=0.5, markeredgecolor='none') 9 | lines2, = ax3.plot(temperatures, texgrid[dens,:], **styleargs) 10 | ax3.set_ylim(0,20) 11 | p3, =ax3.plot(temperatures[tem],texgrid[dens,tem], 'o',alpha=0.5, markeredgecolor='none') 12 | 13 | lines3, = ax2.semilogx(densities, taugrid[:,tem], **styleargs) 14 | ax2.set_ylim(-0.2,0.2) 15 | p2, = ax2.plot(densities[dens],taugrid[dens,tem], 'o',alpha=0.5, markeredgecolor='none') 16 | lines4, = ax4.semilogx(densities, texgrid[:,tem], **styleargs) 17 | p4, = ax4.plot(densities[dens],texgrid[dens,tem], 'o',alpha=0.5, markeredgecolor='none') 18 | ax4.set_ylim(0,20) 19 | plt.suptitle("$T=%i$ K, $n=10^{%0.1f}$ cm$^{-3}$" % (temperatures[tem],np.log10(densities[dens])), 20 | fontsize=20) 21 | ax4.set_xlabel('$n(H_2)$',fontsize=20) 22 | ax3.set_xlabel("T",fontsize=20) 23 | ax1.set_ylabel(r"$\tau$",fontsize=20) 24 | ax3.set_ylabel("$T_{ex}$",fontsize=20) 25 | plt.show() 26 | return fig,lines1,lines2,lines3,lines4,p1,p2,p3,p4 27 | 28 | def run_plot_temden(taugrid=taugrid_303, texgrid=texgrid_303): 29 | fig,lines1,lines2,lines3,lines4,p1,p2,p3,p4 = setup(taugrid=taugrid,texgrid=texgrid) 30 | 31 | @interact(tem=(0,temperatures.size-1),dens=(0,densities.size-1)) 32 | def plot_temden(tem,dens): 33 | lines1.set_data(temperatures, taugrid[dens,:]) 34 | lines2.set_data(temperatures, texgrid[dens,:]) 35 | lines3.set_data(densities, taugrid[:,tem]) 36 | lines4.set_data(densities, texgrid[:,tem]) 37 | p1.set_data(temperatures[tem],taugrid[dens,tem]) 38 | p2.set_data(densities[dens],taugrid[dens,tem]) 39 | p3.set_data(temperatures[tem],texgrid[dens,tem]) 40 | p4.set_data(densities[dens],texgrid[dens,tem]) 41 | 42 | display(fig) 43 | -------------------------------------------------------------------------------- /pyradex/grid_wrapper.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .core import Radex 3 | from astropy.utils.console import ProgressBar 4 | 5 | def grid_wrapper(molecule, 6 | temperatures=[], 7 | densities=[], 8 | columns=[], 9 | abundances=[], 10 | transition_indices=[], 11 | orthopararatios=[], 12 | observable_parameters=['tex','source_line_surfbrightness','tau'], 13 | deltav=1.0, # km/s 14 | ): 15 | """ 16 | Wrapper to build grids 17 | 18 | Parameters 19 | ---------- 20 | """ 21 | 22 | 23 | ntemp = len(temperatures) 24 | ndens = len(densities) 25 | ncols = len(columns) 26 | nabund = len(abundances) 27 | nopr = len(orthopararatios) 28 | 29 | grids = {tid: 30 | {par: np.empty([nopr, ncols, nabund, ntemp, ndens]) 31 | for par in observable_parameters} 32 | for tid in transition_indices} 33 | 34 | # Just a quick first run to get things initialized 35 | if coltype == 'mol': 36 | R = Radex(species=molecule, column=columns[0], abundance=abundances[0]) 37 | R.deltav = deltav 38 | R.run_radex() 39 | 40 | # get the table so we can look at the frequency grid 41 | # table = R.get_table() 42 | 43 | # Target frequencies: 44 | # frequencies = table[np.array(transition_indices)] 45 | 46 | pb = ProgressBar(ntemp*ndens*ncols*nabund*nopr) 47 | 48 | for opri, opr in enumerate(orthopararatios): 49 | fortho = opr/(1+opr) 50 | for coli, col in enumerate(columns): 51 | for abundi, abund in enumerate(abundances): 52 | for temi,tem in enumerate(temperatures): 53 | R.temperature = tem 54 | for densi,dens in enumerate(densities): 55 | R.density = {'oH2':dens*fortho,'pH2':dens*(1-fortho)} 56 | R.column_per_bin = col 57 | R.abundance = abund # reset column to the appropriate value 58 | R.run_radex(reuse_last=False, reload_molfile=True) 59 | 60 | # type is important: MUST be tuple! 61 | ind = (opri, coli, abundi, temi, densi) 62 | 63 | pb.update() 64 | 65 | for tid in transition_indices: 66 | for par in observable_parameters: 67 | val = getattr(R, par)[tid] 68 | if hasattr(val,'value'): 69 | val = val.value 70 | grids[tid][par][ind] = val 71 | 72 | return grids 73 | -------------------------------------------------------------------------------- /examples/compare_co_radex_despotic.py: -------------------------------------------------------------------------------- 1 | import pyradex 2 | import pylab as pl 3 | from astropy import units as u 4 | from pyradex.utils import united,uvalue,get_datafile 5 | import numpy as np 6 | import os 7 | 8 | def test_co(): 9 | mydir = os.path.dirname(os.path.realpath(__file__)) 10 | 11 | print mydir 12 | datapath,speciesdat = get_datafile('co', savedir=os.path.join(mydir,'data/')) 13 | print datapath,speciesdat 14 | 15 | R = pyradex.Radex(species='co', abundance=1e-4, column=None, 16 | temperature=10, 17 | collider_densities={'ph2':990,'oh2':10}, deltav=2.0, 18 | datapath=datapath, escapeProbGeom='lvg') 19 | D = pyradex.despotic_interface.Despotic(hcolumn=2e21, species='co', 20 | temperature=10, 21 | abundance=1e-4, 22 | collider_densities={'ph2':990,'oh2':10}, 23 | deltav=2.0, 24 | datapath=datapath, 25 | escapeProbGeom='lvg') 26 | 27 | # make sure we're dealing with identical qtys 28 | assert uvalue(D.temperature,u.K) == uvalue(R.temperature,u.K) 29 | assert R.density == D.density 30 | #np.testing.assert_approx_equal(uvalue(D.cloud.colDen/2,u.cm**-2), uvalue(R.h2column,u.cm**-2)) 31 | 32 | # make sure RADEX converged 33 | assert R.run_radex() < 200 34 | 35 | print "RADEX N/dv/1.0645: ",R.radex.cphys.cdmol/(R.radex.cphys.deltav/1e5)/1.0645 36 | print "RADEX N/dv/1.27: ",R.radex.cphys.cdmol/(R.radex.cphys.deltav/1e5)/1.27 37 | print "DESPOTIC xs NH / dvdr",D.cloud.emitters['co'].abundance * D.cloud.colDen / (D.cloud.dVdr*3.08e18/1e5) 38 | 39 | TR = R.get_table() 40 | TD = D.get_table(noClump=True) 41 | 42 | R.source_area = 1*u.arcsec**2 43 | 44 | print("DV: D: {0} R: {1}".format(D.deltav, R.deltav)) 45 | print(R.line_flux_density[:5]) 46 | print(R.line_brightness_temperature(1*u.arcsec**2)[:5]) 47 | print(R.source_line_brightness_temperature[:5]) 48 | print(R.T_B[:5]) 49 | 50 | TR[:5]['frequency','Tex','tau','upperlevelpop','lowerlevelpop'].pprint() 51 | TD[:5]['frequency','Tex','tau','upperlevelpop','lowerlevelpop'].pprint() 52 | print(np.array(TR['tau'][:5])) 53 | print(np.array(TD['tau'][:5])) 54 | print(np.array(TR['tau'][:5])/np.array(TD['tau'][:5])) 55 | 56 | return D,R 57 | 58 | def compare_levpops(D,R): 59 | uplevs = {'R':[], 'D': []} 60 | betas = {'R':[], 'D': []} 61 | 62 | for dv in range(1,6): 63 | R.deltav = dv 64 | # 1.27 gets them to very nearly agree, but still not quite, and not consistent. 65 | # I think I'm no closer to an answer. 66 | D.deltav = dv 67 | R.run_radex() 68 | D.recompute() 69 | uplevs['R'].append(R.upperlevelpop) 70 | uplevs['D'].append(D.upperlevelpop) 71 | betas['R'].append(R.beta[:len(D.beta)]) 72 | betas['D'].append(D.beta) 73 | 74 | uplevs['R'] = np.array(uplevs['R']) 75 | uplevs['D'] = np.array(uplevs['D']) 76 | betas['R'] = np.array(betas['R']) 77 | betas['D'] = np.array(betas['D']) 78 | 79 | pl.plot((uplevs['R'][:,:6]-uplevs['D'][:,:6])/uplevs['D'][:,:6]) 80 | #pl.plot(uplevs['D'][:,:4], linestyle=':') 81 | pl.figure(3) 82 | pl.clf() 83 | pl.plot((betas['R'][:,:5]-betas['D'][:,:5]).T, '-') 84 | 85 | return uplevs 86 | 87 | if __name__ == '__main__': 88 | D,R = test_co() 89 | pl.figure(1) 90 | uplevs = compare_levpops(D,R) 91 | pl.figure(2) 92 | pl.plot(D.beta) 93 | pl.plot(R.beta[:len(D.beta)]) 94 | pl.show() 95 | 96 | -------------------------------------------------------------------------------- /examples/ph2co_grids.py: -------------------------------------------------------------------------------- 1 | """ 2 | Create some simple grids for the low-frequency para-H2CO lines 3 | """ 4 | import pylab as pl 5 | import numpy as np 6 | 7 | if not 'texgrid_355M' in locals(): 8 | execfile('ph2co_grid_computation.py') 9 | 10 | pl.figure(1) 11 | pl.clf() 12 | extent = [densities.min(),densities.max(),temperatures.min(),temperatures.max()] 13 | for kk,(grid,freq,label) in enumerate(zip([taugrid_145,taugrid_355M,texgrid_145,texgrid_355M],['145','355M','145','355M'],[r'\tau',r'\tau','T_{ex}','T_{ex}'])): 14 | ax = pl.subplot(2,2,kk+1) 15 | #ax.imshow(grid, extent=extent) 16 | pc = ax.pcolormesh(temperatures,np.log10(densities),grid) 17 | ax.set_title('$%s$ p-H$_2$CO %s GHz' % (label,freq)) 18 | ax.set_ylabel('log Density') 19 | ax.set_xlabel('Temperature') 20 | pl.colorbar(pc) 21 | 22 | pl.figure(7) 23 | pl.clf() 24 | extent = [densities.min(),densities.max(),temperatures.min(),temperatures.max()] 25 | for kk,(grid,freq,label) in enumerate(zip([taugrid_71M,taugrid_355M,texgrid_71M,texgrid_355M],['71M','355M','71M','355M'],[r'\tau',r'\tau','T_{ex}','T_{ex}'])): 26 | ax = pl.subplot(2,2,kk+1) 27 | #ax.imshow(grid, extent=extent) 28 | pc = ax.pcolormesh(temperatures,np.log10(densities),grid, vmax=50 if 'ex' in label else None) 29 | ax.set_title('$%s$ p-H$_2$CO %sHz' % (label,freq)) 30 | ax.set_ylabel('log Density') 31 | ax.set_xlabel('Temperature') 32 | pl.colorbar(pc) 33 | 34 | pl.figure(2) 35 | pl.clf() 36 | ax = pl.subplot(2,1,1) 37 | #ax.imshow(grid, extent=extent) 38 | cax = ax.pcolormesh(temperatures,np.log10(densities),taugrid_145/taugrid_355M, vmax=1e4, vmin=0) 39 | pl.colorbar(cax) 40 | ax.set_title('$\\tau$ p-H$_2$CO 145 GHz/355MHz') 41 | ax.set_ylabel('log Density') 42 | ax.set_xlabel('Temperature') 43 | 44 | ax = pl.subplot(2,1,2) 45 | #ax.imshow(grid, extent=extent) 46 | cax = ax.pcolormesh(temperatures,np.log10(densities),texgrid_145/texgrid_355M)#, vmax=1.3, vmin=0.8) 47 | pl.colorbar(cax) 48 | ax.set_title('$T_{ex}$ p-H$_2$CO 145 GHz/355 MHz') 49 | ax.set_ylabel('log Density') 50 | ax.set_xlabel('Temperature') 51 | 52 | pl.figure(3) 53 | pl.clf() 54 | ax = pl.subplot(2,1,1) 55 | #ax.imshow(grid, extent=extent) 56 | cax = ax.pcolormesh(temperatures,np.log10(densities),taugrid_71M/taugrid_355M, vmax=45, vmin=-2) 57 | pl.colorbar(cax) 58 | ax.set_title('$\\tau$ p-H$_2$CO 71/355 MHz') 59 | ax.set_ylabel('log Density') 60 | ax.set_xlabel('Temperature') 61 | 62 | ax = pl.subplot(2,1,2) 63 | #ax.imshow(grid, extent=extent) 64 | cax = ax.pcolormesh(temperatures,np.log10(densities),texgrid_71M/texgrid_355M, vmax=2, vmin=0.0) 65 | pl.colorbar(cax) 66 | ax.set_title('$T_{ex}$ p-H$_2$CO 71/355 MHz') 67 | ax.set_ylabel('log Density') 68 | ax.set_xlabel('Temperature') 69 | 70 | 71 | pl.figure(4) 72 | pl.clf() 73 | extent = [densities.min(),densities.max(),temperatures.min(),temperatures.max()] 74 | for kk,(freq,name) in enumerate(zip([0.071,145,0.355],['71M','145','355M'])): 75 | ax = pl.subplot(2,2,kk+1) 76 | grid = eval('fluxgrid_%s' % name) 77 | #ax.imshow(grid, extent=extent) 78 | pc = ax.pcolormesh(temperatures,np.log10(densities),grid) 79 | ax.set_title('Flux p-H$_2$CO %i GHz' % (freq)) 80 | ax.set_ylabel('log Density') 81 | ax.set_xlabel('Temperature') 82 | pl.colorbar(pc) 83 | 84 | pl.figure(6) 85 | pl.clf() 86 | ax = pl.subplot(2,1,1) 87 | #ax.imshow(grid, extent=extent) 88 | cax = ax.pcolormesh(temperatures,np.log10(densities),fluxgrid_71M/fluxgrid_355M)#, vmax=0.01, vmin=0) 89 | pl.colorbar(cax) 90 | ax.set_title('$S_{\\nu}$ p-H$_2$CO 71/355 MHz') 91 | ax.set_ylabel('log Density') 92 | ax.set_xlabel('Temperature') 93 | 94 | ax = pl.subplot(2,1,2) 95 | #ax.imshow(grid, extent=extent) 96 | cax = ax.pcolormesh(temperatures,np.log10(densities),fluxgrid_145/fluxgrid_355M)#, vmax=1.2, vmin=0.8) 97 | pl.colorbar(cax) 98 | ax.set_title('$S_{\\nu}$ p-H$_2$CO 145 GHz/355 MHz') 99 | ax.set_ylabel('log Density') 100 | ax.set_xlabel('Temperature') 101 | 102 | 103 | pl.show() 104 | 105 | -------------------------------------------------------------------------------- /examples/fjdu_vs_radex.py: -------------------------------------------------------------------------------- 1 | import pyradex 2 | import pyradex.fjdu 3 | import pylab as pl 4 | import numpy as np 5 | from matplotlib.lines import Line2D 6 | from utils import in_ipynb_kernel 7 | 8 | 9 | for density in ({'oH2': 100, 'pH2': 900, }, 10 | {'oH2': 1000, 'pH2': 9000, }, 11 | {'oH2': 10000, 'pH2': 90000, },): 12 | 13 | RR = pyradex.Radex(species='co', column=1e10, density=density, temperature=20) 14 | FF = pyradex.fjdu.Fjdu(species='co', column=1e10, density=density, temperature=20) 15 | 16 | if in_ipynb_kernel(): 17 | fig1 = pl.figure() 18 | fig2 = pl.figure() 19 | else: 20 | fig1 = pl.figure(1) 21 | fig1.clf() 22 | fig2 = pl.figure(2) 23 | fig2.clf() 24 | fig1,(ax1a,ax1b,ax1c) = pl.subplots(nrows=3, num=1) 25 | fig2,(ax2a,ax2b,ax2c) = pl.subplots(nrows=3, num=2) 26 | 27 | for temperature in (20,500,1000): 28 | for column in (1e12, 1e14, 1e16): 29 | 30 | rtbl = RR(temperature=temperature, column=column, density=density) 31 | ftbl = FF(temperature=temperature, column=column, density=density) 32 | 33 | L1, = ax1a.plot(rtbl['upperlevel'], rtbl['upperlevelpop'], '-', 34 | alpha=0.5, linewidth=2, label='Radex') 35 | L2, = ax1a.plot(ftbl['upperlevel'], ftbl['upperlevelpop'], '--', 36 | alpha=0.5, linewidth=2, label="Fujun Du's MyRadex", 37 | color=L1.get_color()) 38 | ax1a.set_ylabel('Upper Energy Level Population') 39 | 40 | ax1b.plot(rtbl['upperlevel'], rtbl['T_B'], '-', alpha=0.5, linewidth=2, 41 | label='Radex', color=L1.get_color()) 42 | ax1b.semilogy(ftbl['upperlevel'], ftbl['T_B'], '--', alpha=0.5, 43 | linewidth=2, label="Fujun Du's MyRadex", 44 | color=L2.get_color()) 45 | ax1b.set_ylabel('$T_B$') 46 | ax1b.set_ylim(1e-3,2e1) 47 | 48 | ax1c.set_ylabel('tau') 49 | ax1c.semilogy(rtbl['upperlevel'], rtbl['tau'], '-', alpha=0.5, 50 | linewidth=2, label='Radex', color=L1.get_color()) 51 | ax1c.plot(ftbl['upperlevel'], ftbl['tau'], '--', alpha=0.5, 52 | linewidth=2, label="Fujun Du's MyRadex", 53 | color=L2.get_color()) 54 | ax1c.set_xlabel("Upper Energy Level Quantum Number") 55 | ax1c.set_ylim(1e-5,5e0) 56 | 57 | ax2a.plot(rtbl['upperlevel'], 58 | rtbl['upperlevelpop']-ftbl['upperlevelpop'], '-', alpha=0.5, 59 | linewidth=2, color=L1.get_color()) 60 | ax2a.set_ylabel('Upper Energy Level Population') 61 | 62 | ax2b.plot(rtbl['upperlevel'], rtbl['T_B']-ftbl['T_B'], '-', alpha=0.5, 63 | linewidth=2, label='Radex', color=L1.get_color()) 64 | ax2b.set_ylabel('$T_B$') 65 | #ax2b.set_ylim(1e-3,2e1) 66 | 67 | ax2c.set_ylabel('tau') 68 | ax2c.plot(rtbl['upperlevel'], rtbl['tau']-ftbl['tau'], '-', 69 | alpha=0.5, linewidth=2, color=L1.get_color()) 70 | ax2c.set_xlabel("Upper Energy Level Quantum Number") 71 | #ax2c.set_ylim(1e-5,5e0) 72 | 73 | 74 | for ax in (ax1a,ax1b,ax1c,ax2a,ax2b,ax2c): 75 | ax.set_xlim(0,10) 76 | 77 | ax1a.set_title("Density = $10^{{{0}}}$ cm$^{{-3}}$".format(int(np.log10(FF.total_density.value)))) 78 | ax2a.set_title("Difference: Density = $10^{{{0}}}$ cm$^{{-3}}$".format(int(np.log10(FF.total_density.value)))) 79 | ax1a.legend((Line2D([0],[0], linewidth=2, color='k', linestyle='-'), 80 | Line2D([0],[0], linewidth=2, color='k', linestyle='--')), 81 | ('Radex', "Fujun Du's MyRadex"), loc='best') 82 | fig1.savefig("fjdu_vs_radex_CO_n{0}.png".format(int(np.log10(FF.total_density.value)))) 83 | fig2.savefig("fjdu_vs_radex_CO_diffs_n{0}.png".format(int(np.log10(FF.total_density.value)))) 84 | pl.draw() 85 | pl.show() 86 | -------------------------------------------------------------------------------- /INSTALL.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | 4 | Installation *should* be relatively easy, but it may break down depending on 5 | your compiler. 6 | 7 | Installation requires astroquery_ and specutils_. These can be pip installed: 8 | 9 | .. code-block:: bash 10 | 11 | pip install astroquery 12 | pip install https://github.com/astropy/specutils/archive/master.zip 13 | 14 | Compiling RADEX 15 | ~~~~~~~~~~~~~~~ 16 | 17 | This command should install the python-wrapped-fortran version of RADEX: 18 | 19 | .. code-block:: bash 20 | 21 | $ python setup.py install_radex install_myradex install 22 | 23 | If it doesn't work, there are a number of ways things could have gone wrong. 24 | 25 | If you try to `import pyradex` and see the error:: 26 | 27 | ImportError: No module named radex 28 | 29 | that means that the "shared object" file `radex.so` (which is what python 30 | actually imports) never got built. In this case, you should have seen an 31 | error message at the `python setup.py install_radex` step. At this point, 32 | look in the `radex_build.log` file - it may contain useful information. 33 | 34 | I don't know how general this advice is, but for installation on 2 macs and 1 35 | linux machine, the trick was using the right `gfortran` versions with the right 36 | flags. 37 | 38 | If you're using mac-native compilers (e.g., gcc-4.2 from Xcode), you can use 39 | the `-arch` flags. I used this really long command to make sure everything got 40 | set right: 41 | 42 | .. code-block:: bash 43 | 44 | FFLAGS='-arch i686 -arch x86_64 -fPIC' CFLAGS='-fno-strict-aliasing -fno-common -dynamic -arch i386 -arch x86_64 -g -O2' LDFLAGS='-arch i686 -arch x86_64 -undefined dynamic_lookup -bundle' python setup.py install_radex 45 | 46 | If you're using a different compiler, say from hpc.sourceforge.net, you need a different 47 | set of flags, and you need to make sure that `gcc --version` and `gfortran --version` match. 48 | 49 | .. code-block:: bash 50 | 51 | FFLAGS='-m64 -fPIC' CFLAGS='-fno-strict-aliasing -fno-common -dynamic -m64 -g -O2' LDFLAGS='-m64 -undefined dynamic_lookup -bundle' python setup.py install_radex install 52 | 53 | Note that if you're using a 32 bit machine or 32 bit python, you should use 54 | `-m32` instead of `-m64` in those flags. 55 | 56 | You may also need to set your C_INCLUDE_PATH, e.g.: 57 | 58 | .. code-block:: bash 59 | 60 | C_INCLUDE_PATH /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk/usr/include 61 | 62 | (I didn't have to do this, but someone else did - the error that pointed us in this direction was that `stdio.h` couldn't be found) 63 | 64 | For linux, the build failed with gcc-4.0.1 but succeeded with gcc-4.3.6, which 65 | suggests that gcc>=4.2 might be required (since gcc-4.2 worked on a mac). 66 | 67 | 68 | Environment Setup 69 | ~~~~~~~~~~~~~~~~~ 70 | 71 | The shell environmental variable ``RADEX_DATAPATH`` should be set to the 72 | directory in which you store collision rate data files. You can also use the 73 | ``datapath`` keyword argument to specify the path during initialization of the 74 | RADEX object. 75 | 76 | 77 | Installation Troubles 78 | ~~~~~~~~~~~~~~~~~~~~~ 79 | 80 | If you still cannot install pyradex given the above information, please post on 81 | the issues_ page, and include the following information: 82 | 83 | * python version 84 | * numpy version 85 | * astropy version 86 | * gfortran version 87 | 88 | e.g.: 89 | 90 | .. code-block:: bash 91 | 92 | $ python -c "import sys, astropy, numpy; print(sys.version); print(numpy.__version__,astropy.__version__)" 93 | 2.7.2 (v2.7.2:8527427914a2, Jun 11 2011, 15:22:34) 94 | [GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] 95 | ('1.6.1', '0.3.dev6331') 96 | 97 | $ gfortran --version 98 | GNU Fortran (GCC) 4.2.3 99 | Copyright (C) 2007 Free Software Foundation, Inc. 100 | 101 | 102 | .. _issues: https://github.com/keflavich/pyradex/issues 103 | 104 | .. _astroquery: astroquery.readthedocs.org 105 | .. _specutils: https://github.com/astropy/specutils 106 | -------------------------------------------------------------------------------- /pyradex/utils.py: -------------------------------------------------------------------------------- 1 | from astropy import units as u 2 | from six import reraise 3 | from six.moves import zip_longest 4 | import sys 5 | import os 6 | import errno 7 | import itertools 8 | _quantity = u.Quantity 9 | 10 | def mkdir_p(path): 11 | """ mkdir -p equivalent [used by get_datafile]""" 12 | try: 13 | os.makedirs(path) 14 | except OSError as exc: # Python >2.5 15 | if exc.errno == errno.EEXIST and os.path.isdir(path): 16 | pass 17 | else: 18 | raise 19 | 20 | def united(qty, unit): 21 | if isinstance(qty,u.Quantity): 22 | return qty.to(unit) 23 | else: 24 | return qty*u.Unit(unit) 25 | 26 | def uvalue(qty, unit): 27 | return united(qty, unit).value 28 | 29 | def get_datafile(species, savedir='./'): 30 | """ 31 | Load a molecular data file and save it into the specified directory 32 | """ 33 | from astroquery.lamda import Lamda 34 | 35 | datapath = os.path.join(savedir,species) 36 | 37 | species,suffix = os.path.splitext(species) 38 | if suffix == "": 39 | datapath += ".dat" 40 | elif suffix != ".dat": 41 | raise ValueError("Molecular data file must either be a species name or species.dat") 42 | 43 | if not os.path.isdir(savedir): 44 | mkdir_p(savedir) 45 | 46 | if not os.path.isfile(datapath): 47 | data = Lamda.query(species, return_datafile=True) 48 | with open(datapath,'w') as out: 49 | out.writelines([d+"\n" for d in data]) 50 | 51 | return os.path.split(datapath) 52 | 53 | def get_colliders(fn): 54 | """ 55 | Get the list of colliders in a LAMDA data file 56 | """ 57 | from astroquery import lamda 58 | 59 | collrates,radtrans,enlevs = lamda.core.parse_lamda_datafile(fn) 60 | colliders = collrates.keys() 61 | 62 | return colliders 63 | 64 | 65 | def verify_collisionratefile(fn): 66 | """ 67 | Verify that a RADEX collisional rate file is valid to avoid a RADEX crash 68 | """ 69 | from astroquery import lamda 70 | 71 | if not os.path.exists(fn): 72 | raise IOError("File {0} does not exist.".format(fn)) 73 | 74 | for qt in lamda.core.query_types: 75 | try: 76 | collrates,radtrans,enlevs = lamda.core.parse_lamda_datafile(fn) 77 | except Exception as ex: 78 | reraise(type(ex), type(ex)("Data file verification failed. The molecular data file may be corrupt." + 79 | "\nOriginal Error in the parser: " + 80 | ex.args[0]), 81 | sys.exc_info()[2]) 82 | if len(collrates) == 0: 83 | raise ValueError("No data found in the table for the category %s" % qt) 84 | 85 | class QuantityOff(object): 86 | """ Context manager to disable quantities """ 87 | def __enter__(self): 88 | self._quantity = u.Quantity 89 | u.Quantity = lambda value,unit: value 90 | 91 | def __exit__(self, type, value, traceback): 92 | u.Quantity = self._quantity 93 | 94 | class NoVerify(object): 95 | """ Context manager to disable verification of molecule files """ 96 | def __enter__(self): 97 | self._verify_collisionratefile = verify_collisionratefile 98 | globals()['verify_collisionratefile'] = lambda x: True 99 | 100 | def __exit__(self, type, value, traceback): 101 | globals()['verify_collisionratefile'] = self._verify_collisionratefile 102 | 103 | class ImmutableDict(dict): 104 | def __setitem__(self, key, value): 105 | raise AttributeError("Setting items for this dictionary is not supported.") 106 | 107 | def unitless(x): 108 | if hasattr(x, 'value'): 109 | return x.value 110 | else: 111 | return x 112 | 113 | # silly tool needed for fortran misrepresentation of strings 114 | # http://stackoverflow.com/questions/434287/what-is-the-most-pythonic-way-to-iterate-over-a-list-in-chunks 115 | def grouper(iterable, n, fillvalue=None): 116 | args = [iter(iterable)] * n 117 | return zip_longest(*args, fillvalue=fillvalue) 118 | 119 | def lower_keys(d): 120 | """ copy dictionary with lower-case keys """ 121 | return {k.lower(): d[k] for k in d} 122 | -------------------------------------------------------------------------------- /examples/h2cs_thermometer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple proposal-writing experiment: For a given signal-to-noise in a line, what 3 | signal-to-noise do you get in a derived parameter (e.g., temperature)? 4 | """ 5 | import pyradex 6 | import pylab as pl 7 | import numpy as np 8 | import matplotlib 9 | import astropy.units as u 10 | import os 11 | 12 | # for pretty on-screen plots 13 | if os.path.exists('/Users/adam/.matplotlib/ggplotrc'): 14 | matplotlib.rc_file('/Users/adam/.matplotlib/ggplotrc') 15 | 16 | # Download the data file if it's not here already 17 | if not os.path.exists('ph2cs.dat'): 18 | import urllib 19 | import shutil 20 | fn,msg = urllib.urlretrieve('http://home.strw.leidenuniv.nl/~moldata/datafiles/ph2cs.dat') 21 | shutil.move(fn,'ph2cs.dat') 22 | 23 | # Formatting tool 24 | def latex_float(f): 25 | float_str = "{0:.1g}".format(f) 26 | if "e" in float_str: 27 | base, exponent = float_str.split("e") 28 | return r"{0} \times 10^{{{1}}}".format(base, int(exponent)) 29 | else: 30 | return float_str 31 | 32 | # Define the grid parameters 33 | ntemp = 25 34 | temperatures = np.linspace(10,100,ntemp) 35 | 36 | density = 1e4 37 | 38 | abundance = 10**-8.5 # 10**-8.5 39 | for abundance in (10**-8.5,10**-9): 40 | 41 | R = pyradex.Radex(species='ph2cs', 42 | abundance=abundance, 43 | collider_densities={'H2':density}, 44 | column=None, 45 | temperature=temperatures[0]) 46 | 47 | pl.figure(1) 48 | pl.clf() 49 | 50 | for temperature in [10,50,100]: 51 | R.temperature = temperature 52 | R.run_radex() 53 | wcs = np.linspace(103.0, 103.1, 1000)*u.GHz 54 | S = pyradex.synthspec.SyntheticSpectrum.from_RADEX(wcs, R, 55 | linewidth=10*u.km/u.s) 56 | S.plot(label='%i K' % temperature) 57 | 58 | pl.legend(loc='best') 59 | 60 | # create a small grid... 61 | densities = [10**x for x in xrange(4,7)] 62 | ratio = {d:[] for d in densities} 63 | f1 = {d:[] for d in densities} 64 | f2 = {d:[] for d in densities} 65 | 66 | for density in densities: 67 | R.density = {'H2': density} 68 | for temperature in temperatures: 69 | R.temperature = temperature 70 | print R.run_radex(), 71 | 72 | F1 = R.T_B[2] 73 | F2 = R.T_B[12] 74 | 75 | ratio[density].append(F2/F1) 76 | f2[density].append(F2) 77 | f1[density].append(F1) 78 | print 79 | 80 | f1 = {d:np.array([x.value for x in f1[d]]) for d in densities} 81 | f2 = {d:np.array([x.value for x in f2[d]]) for d in densities} 82 | ratio = {d:np.array(ratio[d]) for d in densities} 83 | 84 | pl.figure(2) 85 | pl.clf() 86 | 87 | for d in densities: 88 | pl.plot(ratio[d],temperatures,label='$n=10^{%i}$' % (np.log10(d))) 89 | 90 | m = 1/((ratio[d][15]-ratio[d][5])/(temperatures[15]-temperatures[5])) 91 | b = temperatures[5]-ratio[d][5]*m 92 | line=(m,b) 93 | print d,m,b 94 | pl.plot(ratio[d],ratio[d]*line[0]+line[1],'--') 95 | 96 | pl.ylabel("Temperature") 97 | pl.xlabel("$S(3_{2,1}-2_{2,0})/S(3_{0,3}-2_{0,2})$") 98 | pl.legend(loc='best',fontsize=14) 99 | pl.title("X(p-H$_2$CS)$=10^{%0.1f}$" % (np.log10(abundance))) 100 | 101 | pl.axis([0,0.5,10,200,]) 102 | 103 | pl.savefig("pH2CS_ratio_vs_temperature_X=%0.1e.pdf" % (abundance),bbox_inches='tight') 104 | 105 | pl.figure(3) 106 | pl.clf() 107 | for d in densities: 108 | L, = pl.plot(temperatures,f2[d],label='$n=10^{%i}$' % (np.log10(d))) 109 | pl.plot(temperatures,f1[d],'--',color=L.get_color()) 110 | pl.xlabel("Temperature") 111 | pl.ylabel("$T_B(3_{2,1}-2_{2,0})$ (solid), $T_B(3_{0,3}-2_{0,2})$ (dashed)") 112 | ax = pl.gca() 113 | #pl.plot(ax.get_xlim(),[1.5,1.5],'k--',label='S/N$=5$, $\sigma=0.3$ K') 114 | #pl.plot(ax.get_xlim(),[2.5,2.5],'k:',label='S/N$=5$, $\sigma=0.5$ K') 115 | #pl.plot(ax.get_xlim(),[0.25,0.25],'k-.',label='S/N$=5$, $\sigma=0.05$ K') 116 | ax.axis([10,100,0,3.2]) 117 | pl.legend(loc='best',fontsize=14) 118 | pl.title("X(p-H$_2$CS)$=10^{%0.1f}$" % (np.log10(abundance))) 119 | pl.savefig("pH2CS_321-220_vs_temperature_X=%0.1e.pdf" % (abundance),bbox_inches='tight') 120 | -------------------------------------------------------------------------------- /examples/h2co_grids.py: -------------------------------------------------------------------------------- 1 | import pyradex 2 | import pylab as pl 3 | import numpy as np 4 | import matplotlib 5 | 6 | ntemp,ndens = 20,20 7 | 8 | temperatures = np.linspace(10,50,ntemp) 9 | densities = np.logspace(2.5,7,ndens) 10 | abundance = 10**-8.5 11 | opr = 0.01 # assume primarily para 12 | fortho = opr/(1+opr) 13 | 14 | taugrid_6 = np.empty([ndens,ntemp]) 15 | texgrid_6 = np.empty([ndens,ntemp]) 16 | fluxgrid_6 = np.empty([ndens,ntemp]) 17 | taugrid_140 = np.empty([ndens,ntemp]) 18 | texgrid_140 = np.empty([ndens,ntemp]) 19 | fluxgrid_140 = np.empty([ndens,ntemp]) 20 | taugrid_150 = np.empty([ndens,ntemp]) 21 | texgrid_150 = np.empty([ndens,ntemp]) 22 | fluxgrid_150 = np.empty([ndens,ntemp]) 23 | columngrid = np.empty([ndens,ntemp]) 24 | 25 | import os 26 | if not os.path.exists('oh2co-h2.dat'): 27 | import urllib 28 | urllib.urlretrieve('http://home.strw.leidenuniv.nl/~moldata/datafiles/oh2co-h2.dat') 29 | 30 | R = pyradex.Radex(species='oh2co-h2', abundance=abundance) 31 | R.run_radex() 32 | 33 | # get the table so we can look at the frequency grid 34 | table = R.get_table() 35 | 36 | # Target frequencies: 37 | table[np.array([0,1,3])].pprint() 38 | 39 | for ii,tt in enumerate(temperatures): 40 | R.temperature = tt 41 | for jj,dd in enumerate(densities): 42 | R.density = {'oH2':dd*fortho,'pH2':dd*(1-fortho)} 43 | R.abundance = abundance # reset column to the appropriate value 44 | R.run_radex(reuse_last=False, reload_molfile=True) 45 | 46 | TI = R.source_brightness 47 | taugrid_6[jj,ii] = R.tau[0] 48 | texgrid_6[jj,ii] = R.tex[0].value 49 | fluxgrid_6[jj,ii] = TI[0].value 50 | taugrid_140[jj,ii] = R.tau[1] 51 | texgrid_140[jj,ii] = R.tex[1].value 52 | fluxgrid_140[jj,ii] = TI[1].value 53 | taugrid_150[jj,ii] = R.tau[3] 54 | texgrid_150[jj,ii] = R.tex[3].value 55 | fluxgrid_150[jj,ii] = TI[3].value 56 | columngrid[jj,ii] = R.column.value 57 | 58 | pl.figure(1) 59 | pl.clf() 60 | extent = [densities.min(),densities.max(),temperatures.min(),temperatures.max()] 61 | for kk,(grid,freq,label) in enumerate(zip([taugrid_140,taugrid_150,texgrid_140,texgrid_150],['140','150','140','150'],[r'\tau',r'\tau','T_{ex}','T_{ex}'])): 62 | ax = pl.subplot(2,2,kk+1) 63 | #ax.imshow(grid, extent=extent) 64 | ax.pcolormesh(np.log10(densities),temperatures,grid) 65 | ax.set_title('$%s$ o-H$_2$CO %s GHz' % (label,freq)) 66 | ax.set_xlabel('log Density') 67 | ax.set_ylabel('Temperature') 68 | 69 | pl.figure(2) 70 | pl.clf() 71 | ax = pl.subplot(2,1,1) 72 | #ax.imshow(grid, extent=extent) 73 | cax = ax.pcolormesh(np.log10(densities),temperatures,taugrid_140/taugrid_150, vmax=1.3, vmin=0.8) 74 | pl.colorbar(cax) 75 | ax.set_title('$\\tau$ o-H$_2$CO 140/150 GHz') 76 | ax.set_xlabel('log Density') 77 | ax.set_ylabel('Temperature') 78 | 79 | ax = pl.subplot(2,1,2) 80 | #ax.imshow(grid, extent=extent) 81 | cax = ax.pcolormesh(np.log10(densities),temperatures,texgrid_140/texgrid_150, vmax=1.3, vmin=0.8) 82 | pl.colorbar(cax) 83 | ax.set_title('$T_{ex}$ o-H$_2$CO 140/150 GHz') 84 | ax.set_xlabel('log Density') 85 | ax.set_ylabel('Temperature') 86 | 87 | pl.figure(3) 88 | pl.clf() 89 | ax = pl.subplot(2,1,1) 90 | #ax.imshow(grid, extent=extent) 91 | cax = ax.pcolormesh(np.log10(densities),temperatures,taugrid_6/taugrid_150, vmax=0.5, vmin=0.01) 92 | pl.colorbar(cax) 93 | ax.set_title('$\\tau$ o-H$_2$CO 6/150 GHz') 94 | ax.set_xlabel('log Density') 95 | ax.set_ylabel('Temperature') 96 | 97 | ax = pl.subplot(2,1,2) 98 | #ax.imshow(grid, extent=extent) 99 | cax = ax.pcolormesh(np.log10(densities),temperatures,texgrid_6/texgrid_150, vmax=2, vmin=0.1) 100 | pl.colorbar(cax) 101 | ax.set_title('$T_{ex}$ o-H$_2$CO 6/150 GHz') 102 | ax.set_xlabel('log Density') 103 | ax.set_ylabel('Temperature') 104 | 105 | 106 | pl.figure(4) 107 | pl.clf() 108 | extent = [densities.min(),densities.max(),temperatures.min(),temperatures.max()] 109 | for kk,freq in enumerate([6,140,150]): 110 | ax = pl.subplot(2,2,kk+1) 111 | grid = eval('fluxgrid_%i' % freq) 112 | #ax.imshow(grid, extent=extent) 113 | ax.pcolormesh(np.log10(densities),temperatures,grid) 114 | ax.set_title('Flux o-H$_2$CO %i GHz' % (freq)) 115 | ax.set_xlabel('log Density') 116 | ax.set_ylabel('Temperature') 117 | 118 | pl.figure(6) 119 | pl.clf() 120 | ax = pl.subplot(2,1,1) 121 | #ax.imshow(grid, extent=extent) 122 | cax = ax.pcolormesh(np.log10(densities),temperatures,fluxgrid_6/fluxgrid_150, vmax=0.01, vmin=0) 123 | pl.colorbar(cax) 124 | ax.set_title('$S_{\\nu}$ o-H$_2$CO 6/150 GHz') 125 | ax.set_xlabel('log Density') 126 | ax.set_ylabel('Temperature') 127 | 128 | ax = pl.subplot(2,1,2) 129 | #ax.imshow(grid, extent=extent) 130 | cax = ax.pcolormesh(np.log10(densities),temperatures,fluxgrid_140/fluxgrid_150, vmax=1.2, vmin=0.8) 131 | pl.colorbar(cax) 132 | ax.set_title('$S_{\\nu}$ o-H$_2$CO 140/150 GHz') 133 | ax.set_xlabel('log Density') 134 | ax.set_ylabel('Temperature') 135 | 136 | 137 | pl.show() 138 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import glob 6 | 7 | if any((x in sys.argv for x in ('develop', 'bdist'))): 8 | # use setuptools for develop, but nothing else 9 | from setuptools import setup 10 | from distutils.core import Command 11 | else: 12 | from distutils.core import setup, Command 13 | 14 | with open('README.rst') as file: 15 | long_description = file.read() 16 | 17 | #with open('CHANGES') as file: 18 | # long_description += file.read() 19 | 20 | __version__ = "" 21 | with open("pyradex/version.py") as f: 22 | exec(f.read()) 23 | 24 | #import os 25 | #if not os.path.exists('pyradex/radex/radex.so'): 26 | # import install_radex 27 | # install_radex.install_radex() 28 | 29 | class InstallRadex(Command): 30 | """ 31 | Compile the f2py radex modules needed for the python wrapper 32 | """ 33 | 34 | user_options = [] 35 | 36 | def initialize_options(self): 37 | pass 38 | 39 | def finalize_options(self): 40 | pass 41 | 42 | def run(self): 43 | import install_radex 44 | install_radex.install_radex() 45 | 46 | class BuildRadexExecutable(Command): 47 | """ 48 | Create the files radex_sphere, radex_lvg, radex_slab in the Radex/bin/ 49 | directory 50 | """ 51 | 52 | user_options = [] 53 | 54 | def initialize_options(self): 55 | pass 56 | 57 | def finalize_options(self): 58 | pass 59 | 60 | def run(self): 61 | import install_radex 62 | install_radex.build_radex_executable() 63 | 64 | 65 | class InstallFjdu(Command): 66 | """ 67 | Compile Fujun Du's "myradex" 68 | """ 69 | user_options = [] 70 | 71 | def initialize_options(self): 72 | pass 73 | 74 | def finalize_options(self): 75 | pass 76 | 77 | def run(self): 78 | cwd = os.getcwd() 79 | os.chdir('myRadex') 80 | if os.path.exists('wrapper_my_radex.so'): 81 | os.remove('wrapper_my_radex.so') 82 | os.system('make clean') 83 | os.system('make wrapper') 84 | result = os.system('make sub_trivials.o') 85 | if result != 0: 86 | raise ValueError("Compilation has failed. Check gfortran version?") 87 | os.chdir(cwd) 88 | for fn in glob.glob('myRadex/wrapper_my_radex*.so'): 89 | outpath = 'pyradex/fjdu/{0}'.format(os.path.basename(fn)) 90 | if os.path.exists(outpath): 91 | os.remove(outpath) 92 | os.link(fn, outpath) 93 | os.chdir('myRadex') 94 | os.system('make clean') 95 | os.chdir(cwd) 96 | 97 | 98 | import subprocess 99 | import shutil 100 | import os 101 | 102 | class PyTest(Command): 103 | 104 | user_options = [] 105 | 106 | def initialize_options(self): 107 | pass 108 | 109 | def finalize_options(self): 110 | pass 111 | 112 | def run(self): 113 | 114 | if os.path.exists('build'): 115 | shutil.rmtree('build') 116 | #errno1 = subprocess.call(['py.test','--genscript=runtests.py']) 117 | errno2 = subprocess.call([sys.executable, 'runtests.py']) 118 | raise SystemExit(errno2) 119 | 120 | radex_shared_object_files = [os.path.basename(fn) for fn in glob.glob("pyradex/radex/*.so")] 121 | myradex_shared_object_files = [os.path.basename(fn) for fn in glob.glob("pyradex/fjdu/*.so")] 122 | 123 | print(f"Found shared object files={radex_shared_object_files} for RADEX. (if that is a blank, it means radex didn't install successfully)") 124 | print(f"Found shared object files={myradex_shared_object_files} for RADEX. (if that is a blank, it means fjdu's myradex didn't install successfully)") 125 | 126 | setup(name='pyradex', 127 | version=__version__, 128 | description='Python-RADEX', 129 | long_description=long_description, 130 | author='Adam Ginsburg & Julia Kamenetzky', 131 | author_email='adam.g.ginsburg@gmail.com', 132 | url='http://github.com/keflavich/pyradex/', 133 | packages=['pyradex','pyradex.radex','pyradex.tests','pyradex.fjdu'], 134 | package_data={'pyradex.radex':['radex.so'] + radex_shared_object_files, 135 | 'pyradex.fjdu':['wrapper_my_radex.so'] + myradex_shared_object_files, 136 | 'pyradex.tests':['data/example.out']}, 137 | requires=['requests', 'astroquery', ], 138 | install_requires=['astropy>=0.4.1', 'requests>=2.4.1',], 139 | cmdclass={'test': PyTest, 140 | 'install_radex': InstallRadex, 141 | 'build_radex_exe': BuildRadexExecutable, 142 | 'install_myradex': InstallFjdu, 143 | 'install_fjdu': InstallFjdu, 144 | }, 145 | #include_package_data=True, 146 | zip_safe=False 147 | ) 148 | 149 | if os.getenv('RADEX_DATAPATH'): 150 | print("Installation has completed. RADEX_DATAPATH={0}".format(os.getenv('RADEX_DATAPATH'))) 151 | else: 152 | print("Installation has completed. Make sure to set your RADEX_DATAPATH variable!") 153 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | env: 4 | global: 5 | # The following versions are the 'default' for tests, unless 6 | # overidden underneath. They are defined here in order to save having 7 | # to repeat them for all configurations. 8 | - NUMPY_VERSION=1.9 9 | - ASTROPY_VERSION=stable 10 | - CONDA_INSTALL='conda install -c astropy-ci-extras --yes' 11 | - PIP_INSTALL='pip install' 12 | 13 | matrix: 14 | include: 15 | 16 | # Do a coverage test in Python 2. This requires the latest 17 | # development version of Astropy, which fixes some issues with 18 | # coverage testing in affiliated packages. 19 | #- python: 2.7 20 | # env: ASTROPY_VERSION=development SETUP_CMD='test --coverage' 21 | 22 | # Check for sphinx doc build warnings - we do this first because it 23 | # may run for a long time 24 | #- python: 2.7 25 | # env: SETUP_CMD='build_sphinx -w' 26 | 27 | # Try Astropy development version 28 | - python: 3.7 29 | env: ASTROPY_VERSION=development SETUP_CMD='test' 30 | #- python: 3.3 31 | # env: ASTROPY_VERSION=development SETUP_CMD='test' 32 | 33 | # Try all python versions with the latest numpy 34 | - python: 3.7 35 | env: SETUP_CMD='test' 36 | #- python: 3.3 37 | # env: SETUP_CMD='test' 38 | #- python: 3.4 39 | # env: SETUP_CMD='test' 40 | 41 | # Try older python versions 42 | - python: 2.7 43 | env: SETUP_CMD='test' 44 | 45 | before_install: 46 | 47 | # Use utf8 encoding. Should be default, but this is insurance against 48 | # future changes 49 | - export PYTHONIOENCODING=UTF8 50 | 51 | # http://conda.pydata.org/docs/travis.html#the-travis-yml-file 52 | - wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; 53 | - bash miniconda.sh -b -p $HOME/miniconda 54 | - export PATH="$HOME/miniconda/bin:$PATH" 55 | - hash -r 56 | - conda config --set always_yes yes --set changeps1 no 57 | - conda update -q conda 58 | - conda info -a 59 | 60 | # DOCUMENTATION DEPENDENCIES 61 | - if [[ $SETUP_CMD == build_sphinx* ]]; then sudo apt-get install graphviz texlive-latex-extra dvipng; fi 62 | 63 | install: 64 | # Fortran needed for radex building 65 | - sudo apt-get update -qq 66 | - sudo apt-get install -qq python-numpy cython libatlas-dev liblapack-dev gfortran 67 | - sudo apt-get install -qq python-scipy 68 | - sudo apt-get install -qq cython 69 | 70 | # CONDA 71 | - conda create --yes -n test -c astropy-ci-extras python=$TRAVIS_PYTHON_VERSION 72 | - source activate test 73 | 74 | # CORE DEPENDENCIES 75 | - if [[ $SETUP_CMD != egg_info ]]; then $CONDA_INSTALL numpy=$NUMPY_VERSION pytest pip Cython jinja2; fi 76 | - if [[ $SETUP_CMD != egg_info ]]; then $PIP_INSTALL pytest-xdist; fi 77 | 78 | # ASTROPY 79 | - if [[ $SETUP_CMD != egg_info ]] && [[ $ASTROPY_VERSION == development ]]; then $PIP_INSTALL git+http://github.com/astropy/astropy.git#egg=astropy; fi 80 | - if [[ $SETUP_CMD != egg_info ]] && [[ $ASTROPY_VERSION == stable ]]; then $CONDA_INSTALL numpy=$NUMPY_VERSION astropy; fi 81 | 82 | # OPTIONAL DEPENDENCIES 83 | # Here you can add any dependencies your package may have. You can use 84 | # conda for packages available through conda, or pip for any other 85 | # packages. You should leave the `numpy=$NUMPY_VERSION` in the `conda` 86 | # install since this ensures Numpy does not get automatically upgraded. 87 | - if [[ $SETUP_CMD != egg_info ]]; then $CONDA_INSTALL numpy=$NUMPY_VERSION requests ; fi 88 | - if [[ ($SETUP_CMD != egg_info) && ($TRAVIS_PYTHON_VERSION == 2.7) ]]; then $CONDA_INSTALL numpy=$NUMPY_VERSION keyring ; fi 89 | - if [[ ($SETUP_CMD != egg_info) && ($TRAVIS_PYTHON_VERSION != 2.7) ]]; then $PIP_INSTALL keyring ; fi 90 | - if [[ $SETUP_CMD != egg_info ]]; then $CONDA_INSTALL numpy=$NUMPY_VERSION cython requests ; fi 91 | # - if [[ $SETUP_CMD != egg_info ]]; then $PIP_INSTALL ...; fi 92 | 93 | - if [[ $SETUP_CMD != egg_info ]] ; then $PIP_INSTALL git+http://github.com/astropy/astroquery.git#egg=astroquery; fi 94 | - if [[ $SETUP_CMD != egg_info ]] ; then $PIP_INSTALL git+http://github.com/astropy/specutils.git#egg=specutils; fi 95 | 96 | # DOCUMENTATION DEPENDENCIES 97 | # build_sphinx needs sphinx and matplotlib (for plot_directive). Note that 98 | # this matplotlib will *not* work with py 3.x, but our sphinx build is 99 | # currently 2.7, so that's fine 100 | - if [[ $SETUP_CMD == build_sphinx* ]]; then $CONDA_INSTALL numpy=$NUMPY_VERSION Sphinx=1.2.2 matplotlib; fi 101 | 102 | # COVERAGE DEPENDENCIES 103 | - if [[ $SETUP_CMD == 'test --coverage' ]]; then $PIP_INSTALL coverage coveralls; fi 104 | 105 | - if [[ $SETUP_CMD != egg_info ]]; then python setup.py build_radex_exe; fi 106 | - if [[ $SETUP_CMD != egg_info ]]; then python setup.py install_radex; fi 107 | - if [[ $SETUP_CMD != egg_info ]]; then CFLAGS='-fPIC' python setup.py install_myradex; fi 108 | - if [[ $SETUP_CMD != egg_info ]]; then python setup.py install; fi 109 | 110 | script: 111 | - python setup.py $SETUP_CMD 112 | 113 | after_success: 114 | # If coveralls.io is set up for this package, uncomment the line 115 | # below and replace "packagename" with the name of your package. 116 | # The coveragerc file may be customized as needed for your package. 117 | - if [[ $SETUP_CMD == 'test --coverage' ]]; then coveralls --rcfile='astroquery/tests/coveragerc'; fi 118 | -------------------------------------------------------------------------------- /examples/timing.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | import numpy as np 3 | import textwrap 4 | import warnings 5 | warnings.filterwarnings('ignore') 6 | from astropy import log 7 | log.setLevel(1000) 8 | 9 | # Check correctness before doing timing tests 10 | import pyradex 11 | py_pop = [pyradex.pyradex(collider_densities={'oH2':900,'pH2':100},column=n, temperature=20)['pop_up'][0] 12 | for n in 10**np.arange(12,18)] 13 | 14 | R = pyradex.Radex(collider_densities={'oH2':900,'pH2':100}, column=1e15, temperature=20) 15 | 16 | R_noreload_pop = [] 17 | for n in 10**np.arange(12,18): 18 | R.column = n 19 | R.run_radex(reload_molfile=False, validate_colliders=False) 20 | R_noreload_pop.append(R.level_population[1]) 21 | 22 | R_pop = [] 23 | for n in 10**np.arange(12,18): 24 | R.column = n 25 | R.run_radex(reload_molfile=True, validate_colliders=True) 26 | R_pop.append(R.level_population[1]) 27 | 28 | R_reuse_pop = [] 29 | for n in 10**np.arange(12,18): 30 | R.column = n 31 | R.run_radex(reload_molfile=False, validate_colliders=False, reuse_last=True) 32 | R_reuse_pop.append(R.level_population[1]) 33 | 34 | for p1,p2,p3,p4 in zip(py_pop, R_noreload_pop, R_pop, R_reuse_pop): 35 | np.testing.assert_almost_equal(p1, p2, decimal=4) 36 | np.testing.assert_almost_equal(p2, p3) 37 | np.testing.assert_almost_equal(p3, p4) 38 | 39 | 40 | 41 | for n in 10**np.arange(12,18): 42 | 43 | setup = "import pyradex" 44 | nreps = 10 45 | ptiming = timeit.Timer(stmt="pyradex.pyradex(collider_densities={'oH2':900,'pH2':100},column=%e,temperature=20)" % n,setup=setup).repeat(3,nreps) 46 | print "Python external call: ",np.min(ptiming)/nreps 47 | setup = """ 48 | import pyradex 49 | R = pyradex.Radex(collider_densities={'oH2':900,'pH2':100}, column=%e, temperature=20)""" % n 50 | ftiming = timeit.Timer(stmt="R.run_radex(); T = R.tex",setup=textwrap.dedent(setup)).repeat(3,nreps) 51 | print "Fortran-wrapped: ",np.min(ftiming)/nreps 52 | ftiming3 = timeit.Timer(stmt="R.run_radex(validate_colliders=False, reload_molfile=False); T = R.tex",setup=textwrap.dedent(setup)).repeat(3,nreps) 53 | print "Fortran-wrapped, no reload: ",np.min(ftiming3)/nreps 54 | ftiming4 = timeit.Timer(stmt="R.run_radex(validate_colliders=False, reload_molfile=False, reuse_last=True); T = R.tex",setup=textwrap.dedent(setup)).repeat(3,nreps) 55 | print "Fortran-wrapped, no reload, reuse: ",np.min(ftiming4)/nreps 56 | # dominated by array creation... 57 | ftiming2 = timeit.Timer(stmt="R(collider_densities={'oH2':900,'pH2':100}, column=%e)" % n, setup=textwrap.dedent(setup)).repeat(3,nreps) 58 | print "Fortran (call method): ",np.min(ftiming2)/nreps 59 | print "py/fortran: ",np.min(ptiming)/np.min(ftiming) 60 | print "py/fortran, __call__ method: ",np.min(ptiming)/np.min(ftiming2) 61 | print "py/fortran, no reload: ",np.min(ptiming)/np.min(ftiming3) 62 | print "py/fortran, no reload, reuse: ",np.min(ptiming)/np.min(ftiming4) 63 | 64 | gridtest = """ 65 | # build a small grid 66 | for ii,T in enumerate([5,10,20]): 67 | for jj,column in enumerate([1e13,1e15,1e17]): 68 | for kk,density in enumerate([1e3,1e5,1e7]): 69 | for mm,opr in enumerate([1e-2,0.1,1]): 70 | fortho = opr/(1+opr) 71 | result = {caller}(collider_densities={{'oH2':density*fortho,'pH2':density*(1-fortho)}}, 72 | column=column, 73 | temperature=T, 74 | species='co') 75 | grid[ii,jj,kk,mm] = result['tau'][0] 76 | print grid[0,0,0,:] 77 | """ 78 | 79 | setup = """ 80 | import pyradex 81 | import numpy as np 82 | grid = np.empty([3,3,3,3]) 83 | """ 84 | ptiming = timeit.Timer(stmt=gridtest.format(caller='pyradex.pyradex'),setup=setup).repeat(3,1) 85 | print "pyradex.pyradex timing for a 3^4 grid: ",ptiming 86 | setup += "R = pyradex.Radex(column=1e15, density=1e4, temperature=20)" 87 | ftiming = timeit.Timer(stmt=gridtest.format(caller='R'),setup=setup).repeat(3,1) 88 | print "pyradex.Radex() timing for a 3^4 grid: ",ftiming 89 | 90 | gridtest_class = """ 91 | # build a small grid 92 | for ii,T in enumerate([5,10,20]): 93 | for jj,column in enumerate([1e13,1e15,1e17]): 94 | for kk,density in enumerate([1e3,1e5,1e7]): 95 | for mm,opr in enumerate([1e-2,0.1,1]): 96 | fortho = opr/(1+opr) 97 | R.density = {'oH2':density*fortho,'pH2':density*(1-fortho)} 98 | R.column=column 99 | R.temperature=T 100 | R.run_radex(validate_colliders=False, 101 | reload_molfile=False, 102 | reuse_last=True) 103 | grid[ii,jj,kk,mm] = R.tau[0] 104 | print grid[0,0,0,:] 105 | """ 106 | 107 | ftiming2 = timeit.Timer(stmt=gridtest_class,setup=setup).repeat(3,1) 108 | print "pyradex.Radex() class-based timing for a 3^4 grid: ",ftiming2 109 | 110 | gridtest_class_faster = """ 111 | # build a small grid 112 | for ii,T in enumerate([5,10,20]): 113 | R.temperature=T 114 | for kk,density in enumerate([1e3,1e5,1e7]): 115 | for mm,opr in enumerate([1e-2,0.1,1]): 116 | fortho = opr/(1+opr) 117 | R.density = {'oH2':density*fortho,'pH2':density*(1-fortho)} 118 | for jj,column in enumerate([1e13,1e15,1e17]): 119 | R.column=column 120 | R.run_radex(validate_colliders=False, 121 | reload_molfile=False, 122 | reuse_last=True) 123 | grid[ii,jj,kk,mm] = R.tau[0] 124 | print grid[0,0,0,:] 125 | """ 126 | 127 | ftiming3 = timeit.Timer(stmt=gridtest_class_faster, setup=setup).repeat(3,1) 128 | print "pyradex.Radex() class-based timing for a 3^4 grid, using optimal parameter-setting order: ",ftiming3 129 | -------------------------------------------------------------------------------- /docs/notes.rst: -------------------------------------------------------------------------------- 1 | A comparison of DESPOTIC and RADEX 2 | ---------------------------------- 3 | 4 | This is a collection of notes on the internal implementation of the escape 5 | probability approximation within RADEX and DESPOTIC. It's mostly a collection 6 | of 'notes to self' to help me understand how the individual parameters are 7 | defined. 8 | 9 | Math versions of despotic and radex: 10 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | 12 | Eqn 39 of Krumholz 2013 vs Eqn 21 of van der Tak 2007: 13 | 14 | .. math:: 15 | 16 | \tau_D = \tau_{ij,LVG} = \frac{g_i}{g_j} \frac{A_{ij} \lambda_{ij}^3}{8 \pi |dv_r/dr|} x_s N_H f_j \left(1-\frac{f_i g_j}{f_j g_i}\right) 17 | 18 | \tau_R = \tau = \frac{c^3}{8\pi \nu_{ul}^3} \frac{A_{ul} N_{mol}}{1.064 \Delta V} \left[ x_l \frac{g_u}{g_l} - x_u \right] 19 | 20 | Footnote 6 in RADEX, combined with footnote 11 in DESPOTIC: 21 | 22 | .. math:: 23 | 24 | \tau_R = 1.5 \tau_D (spherical) 25 | 26 | \tau_R = \tau_D (LVG, slab?) 27 | 28 | Assuming the two are equal, we get the constants dropping out, and are left with 29 | 30 | .. math:: 31 | 32 | \frac{x_s N_H}{|dv_r/dr|} = \frac{N_{mol}}{1.064 \Delta V} 33 | 34 | which approximately defines what is required to get equivalent results out of 35 | DESPOTIC and RADEX. 36 | 37 | 38 | RADEX: 39 | """""" 40 | 41 | The input parameters are :math:`cdmol`, :math:`deltav`, :math:`dens`. The 42 | density only affects the collision rates; it only affects the optical depth 43 | through :math:`crate` and the implicit coupling to column density via the 44 | abundance. 45 | 46 | .. math:: 47 | xt = xnu^3 = (f/c)^3 48 | 49 | cdmol \equiv X_{mol} n(H_2) 50 | 51 | X_{mol} = cdmol / n(H_2) 52 | 53 | cddv = cdmol / deltav = \frac{N_{mol}}{dV} 54 | 55 | dVdR_{RADEX} = deltav = FWHM 56 | 57 | \tau_{RADEX} = \frac{cddv}{(1.0645 (8.0 \pi))} A_{ul} (c/\nu)^{3} (n_l \frac{g_u}{g_l} - n_u) 58 | 59 | = \frac{1}{1.0645 (8.0 \pi) dV_{RADEX}} A_{ul} (c/\nu)^{3} (n_l \frac{g_u}{g_l} - n_u) N_{mol} 60 | 61 | \tau_h = \tau_{RADEX}/2 62 | 63 | \beta = 2.0 \frac{1.0 - e^{-2.34 \tau_h}}{4.68 \tau_h} 64 | 65 | = \frac{1.0 - e^{-1.17 \tau_{RADEX}}}{1.17 \tau_{RADEX}} 66 | 67 | We can factor the 2.35 into the tau equation to get something a little more self-consistent 68 | 69 | .. math:: 70 | 71 | \tau_{RADEX,f} = \frac{2.35}{2 \cdot 1.0645 (8.0 \pi) dV_{RADEX}} A_{ul} (c/\nu)^{3} (n_l \frac{g_u}{g_l} - n_u) N_{mol} 72 | 73 | \tau_{RADEX,f} = \frac{8 \log(2)}{2 \sqrt(2 \pi) (8.0 \pi) dV_{RADEX}} A_{ul} (c/\nu)^{3} (n_l \frac{g_u}{g_l} - n_u) N_{mol} 74 | 75 | DESPOTIC: 76 | """"""""" 77 | 78 | 79 | .. math:: 80 | 81 | dV = thisCloud.dVdr = \frac{dV}{dR} = \sigma 82 | 83 | n \cdot dR = N 84 | 85 | X_{H2} = 2 X_H 86 | 87 | n_H = n_{H2} / 2 88 | 89 | \tau_{DESPOTIC} = \frac{1}{(8 \pi dV_D/dR)} \frac{g_u}{g_l} A_{ul} (c/\nu)^3 \left(1-\frac{n_u g_l}{n_l g_u}\right) X_{H2} n_H n_l 90 | 91 | = \frac{dR}{(8 \pi dV_D)} A_{ul} (c/\nu)^3 \left(n_l\frac{g_u}{g_l}-n_u\right) \frac{n_{mol}}{2} 92 | 93 | = \frac{1}{(8 \pi dV_D)} A_{ul} (c/\nu)^3 \left(n_l\frac{g_u}{g_l}-n_u\right) \frac{N_{mol}}{2} 94 | 95 | 96 | \beta = \frac{1.0 - e^{-\tau_D}}{\tau_D} 97 | 98 | 99 | RADEX and DESPOTIC are equivalent if 100 | 101 | .. math:: 102 | \tau(RADEX,f) = \tau(DESPOTIC) \frac{2.35}{1.0645} 103 | 104 | but so far it looks like 105 | 106 | 107 | If `dV` is given as a fixed quantity to both, in order to make them equivalent, we can do: 108 | 109 | .. math:: 110 | \tau(RADEX,f) = \tau_N \frac{2.35}{2 \cdot 1.0645} 111 | 112 | \tau(DESPOTIC) = \tau_N \frac{1}{2} 113 | 114 | \tau(RADEX,f)/\tau(DESPOTIC) = \frac{2.35}{1.0645} 115 | 116 | To fix it, free the dV variable 117 | 118 | .. math:: 119 | 120 | \tau(RADEX,f)/\tau(DESPOTIC) = 1 121 | = \frac{2.35}{2 \cdot 1.0645 dV_{RADEX}} / \frac{1}{2 dVdr_D} 122 | 123 | dVdr_D = \frac{1.0645}{2.35} dV_{RADEX} 124 | 125 | 126 | One factor of 2 comes from defining a cloud *radius* vs a cloud *diameter*. 127 | Because DESPOTIC was written do determine line transfer from the center of a 128 | cloud outward, it uses the radius. RADEX uses the diameter. However, DESPOTIC 129 | and RADEX also define their abundances differently 130 | 131 | 1.0645 is the ratio of the integral of a Gaussian to the FWHM, i.e. 132 | 133 | .. math:: 134 | \int_{-\infty}^{\infty}\frac{1}{\sqrt{8 \log(2)}} e^{-x^2/2} dx = \sqrt{\frac{2 \pi}{8 \log(2)}} = 1.0644670194312262 135 | 136 | Code versions: 137 | ~~~~~~~~~~~~~~ 138 | 139 | DESPOTIC: 140 | """"""""" 141 | 142 | 143 | .. code-block:: python 144 | 145 | elif escapeProbGeom == "LVG": 146 | tau = (self.data.levWgt[u]/self.data.levWgt[l]) * \ 147 | self.data.EinsteinA[u,l] * (c/self.data.freq[u,l])**3 / \ 148 | (8.0*np.pi*abs(thisCloud.dVdr)) * \ 149 | self.abundance * thisCloud.nH * self.levPop[l] * \ 150 | (1.0 - self.levPop[u]*self.data.levWgt[l] / \ 151 | (self.levPop[l]*self.data.levWgt[u])) 152 | # Note that, in computing escape probabilities, we need to 153 | # handle the case of very small tau with care to avoid 154 | # roundoff problems and make sure we correctly limit to 155 | # beta -> 1 as tau -> 0 156 | idx = tau > 1e-6 157 | self.escapeProb[u[idx], l[idx]] = \ 158 | (1.0 - np.exp(-tau[idx])) / tau[idx] 159 | idx = tau <= 1e-6 160 | self.escapeProb[u[idx], l[idx]] = \ 161 | 1.0 - tau[idx]/2.0 162 | 163 | RADEX: 164 | """""" 165 | 166 | 167 | .. code-block:: fortran 168 | 169 | 170 | xt = xnu(iline)**3.0 171 | m = iupp(iline) 172 | n = ilow(iline) 173 | 174 | taul(iline) = cddv*(xpop(n)*gstat(m)/gstat(n)-xpop(m)) 175 | $ /(fgaus*xt/aeinst(iline)) 176 | 177 | beta = escprob(taul(iline)) 178 | 179 | taur = tau/2.0 180 | 181 | else if (method.eq.2) then 182 | c Expanding sphere = Large Velocity Gradient (LVG) or Sobolev case. 183 | C Formula from De Jong, Boland and Dalgarno (1980, A&A 91, 68) 184 | C corrected by factor 2 in order to match ESCPROB(TAU=0)=1 185 | if (abs(taur).lt.0.01) then 186 | beta = 1.0 187 | else if(abs(taur).lt.7.0) then 188 | beta = 2.0*(1.0 - dexp(-2.34*taur))/(4.68*taur) 189 | else 190 | beta = 2.0/(taur*4.0*(sqrt(log(taur/sqrt(pi))))) 191 | endif 192 | 193 | -------------------------------------------------------------------------------- /examples/ph2co_required_sn.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple proposal-writing experiment: For a given signal-to-noise in a line, what 3 | signal-to-noise do you get in a derived parameter (e.g., temperature)? 4 | """ 5 | import pyradex 6 | import pylab as pl 7 | import numpy as np 8 | import matplotlib 9 | import astropy.units as u 10 | import os 11 | 12 | # for pretty on-screen plots 13 | if os.path.exists('/Users/adam/.matplotlib/ggplotrc'): 14 | matplotlib.rc_file('/Users/adam/.matplotlib/ggplotrc') 15 | 16 | # Download the data file if it's not here already 17 | if not os.path.exists('ph2co-h2.dat'): 18 | import urllib 19 | urllib.urlretrieve('http://home.strw.leidenuniv.nl/~moldata/datafiles/ph2co-h2.dat') 20 | 21 | # Formatting tool 22 | def latex_float(f): 23 | float_str = "{0:.1g}".format(f) 24 | if "e" in float_str: 25 | base, exponent = float_str.split("e") 26 | return r"{0} \times 10^{{{1}}}".format(base, int(exponent)) 27 | else: 28 | return float_str 29 | 30 | # Define the grid parameters 31 | ntemp = 75 32 | tmin = 10 33 | tmax = 300 34 | temperatures = np.linspace(tmin,300,ntemp) 35 | 36 | # initial density; will be modified later 37 | density = 1e4 38 | 39 | deltav = 1.0 # km/s 40 | 41 | for abundance in (10**-8.5,10**-9): 42 | 43 | R = pyradex.Radex(species='ph2co-h2', 44 | abundance=abundance, 45 | collider_densities={'oH2':density,'pH2':0}, 46 | deltav=1.0, 47 | column=None, 48 | temperature=temperatures[0]) 49 | 50 | nh2 = (R.column / R.abundance).value 51 | 52 | pl.figure(1) 53 | pl.clf() 54 | 55 | Swcs = pyradex.synthspec.FrequencyArray(218.2*u.GHz, 218.8*u.GHz, npts=1000) 56 | for temperature in [10,50,100,200,300]: 57 | R.temperature = temperature 58 | R.run_radex() 59 | S = pyradex.synthspec.SyntheticSpectrum.from_RADEX(Swcs, R, linewidth=10*u.km/u.s) 60 | S.plot(label='%i K' % temperature, linewidth=2, alpha=0.5) 61 | 62 | pl.legend(loc='best') 63 | pl.savefig("pH2CO_synthspectra_N=%1.0e_X=%0.1e_n=%0.1e_opr=0.pdf" % (nh2,abundance,density),bbox_inches='tight') 64 | 65 | # create a small grid... 66 | densities = [10**x for x in xrange(4,7)] 67 | ratio1 = {d:[] for d in densities} 68 | ratio2 = {d:[] for d in densities} 69 | f1 = {d:[] for d in densities} 70 | f2 = {d:[] for d in densities} 71 | f3 = {d:[] for d in densities} 72 | 73 | for density in densities: 74 | R.density = {'oH2': density, 'pH2':0} 75 | for temperature in temperatures: 76 | R.temperature = temperature 77 | print R.run_radex(), 78 | 79 | F1 = R.T_B[2] # 218.222192 3_0_3 80 | F2 = R.T_B[12] # 218.760066 3_2_1 81 | F3 = R.T_B[9] # 218.475632 3_2_2 82 | 83 | ratio1[density].append(F2/F1) 84 | ratio2[density].append(F3/F1) 85 | f3[density].append(F3) 86 | f2[density].append(F2) 87 | f1[density].append(F1) 88 | print 89 | 90 | f1 = {d:np.array([x.value for x in f1[d]]) for d in densities} 91 | f2 = {d:np.array([x.value for x in f2[d]]) for d in densities} 92 | f3 = {d:np.array([x.value for x in f3[d]]) for d in densities} 93 | ratio1 = {d:np.array(ratio1[d]) for d in densities} 94 | ratio2 = {d:np.array(ratio2[d]) for d in densities} 95 | ratio = ratio1 96 | 97 | pl.figure(2) 98 | pl.clf() 99 | 100 | for d in densities: 101 | pl.plot(ratio[d],temperatures,label='$n=10^{%i}$' % (np.log10(d))) 102 | 103 | m = 1/((ratio[d][15]-ratio[d][5])/(temperatures[15]-temperatures[5])) 104 | b = temperatures[5]-ratio[d][5]*m 105 | line=(m,b) 106 | print d,m,b 107 | pl.plot(ratio[d],ratio[d]*line[0]+line[1],'--') 108 | 109 | pl.ylabel("Temperature") 110 | pl.xlabel("$S(3_{2,1}-2_{2,0})/S(3_{0,3}-2_{0,2})$") 111 | pl.legend(loc='best',fontsize=14) 112 | pl.title("$N(H_2) = %s$ cm$^{-2}$, X(p-H$_2$CO)$=10^{%0.1f}$" % (latex_float(nh2),np.log10(abundance))) 113 | 114 | pl.axis([0,0.5,tmin,tmax,]) 115 | 116 | pl.savefig("pH2CO_ratio_vs_temperature_N=%1.0e_X=%0.1e.pdf" % (nh2,abundance),bbox_inches='tight') 117 | 118 | pl.figure(3) 119 | pl.clf() 120 | 121 | for d in densities: 122 | L, = pl.plot(temperatures,f2[d],label='$n=10^{%i}$' % (np.log10(d))) 123 | pl.plot(temperatures,f1[d],'--',color=L.get_color()) 124 | pl.xlabel("Temperature") 125 | pl.ylabel("$T_B(3_{2,1}-2_{2,0})$ (solid), $T_B(3_{0,3}-2_{0,2})$ (dashed)") 126 | ax = pl.gca() 127 | #pl.plot(ax.get_xlim(),[1.5,1.5],'k--',label='S/N$=5$, $\sigma=0.3$ K') 128 | #pl.plot(ax.get_xlim(),[2.5,2.5],'k:',label='S/N$=5$, $\sigma=0.5$ K') 129 | #pl.plot(ax.get_xlim(),[0.25,0.25],'k-.',label='S/N$=5$, $\sigma=0.05$ K') 130 | ax.axis([tmin,tmax,0,5.2]) 131 | pl.legend(loc='best',fontsize=14) 132 | pl.title("$N(H_2) = %s$ cm$^{-2}$, X(p-H$_2$CO)$=10^{%0.1f}$" % (latex_float(nh2),np.log10(abundance))) 133 | 134 | pl.savefig("pH2CO_321-220_vs_temperature_N=%1.0e_X=%0.1e.pdf" % (nh2,abundance),bbox_inches='tight') 135 | 136 | 137 | pl.figure(4,figsize=(10,10)) 138 | pl.clf() 139 | ax1= pl.subplot(2,1,1) 140 | 141 | for d in densities: 142 | pl.plot(temperatures,ratio[d],label='$n=10^{%i}$' % (np.log10(d))) 143 | #pl.xlabel("Temperature") 144 | pl.ylabel("$S(3_{2,1}-2_{2,0})/S(3_{0,3}-2_{0,2})$") 145 | pl.legend(loc='upper left',fontsize=18) 146 | pl.title("$N(H_2) = %s$ cm$^{-2}$, X(p-H$_2$CO)$=10^{%0.1f}$" % (latex_float(nh2),np.log10(abundance))) 147 | ax1.set_xticks([]) 148 | pl.subplots_adjust(hspace=0.0) 149 | 150 | 151 | ax2 = pl.subplot(2,1,2) 152 | 153 | 154 | for d in densities: 155 | L, = pl.plot(temperatures,f2[d],dashes=[2,2],label='$n=10^{%i}$' % (np.log10(d))) 156 | pl.plot(temperatures,f1[d],'--',color=L.get_color()) 157 | pl.xlabel("Temperature") 158 | pl.ylabel("$T_B$") 159 | ax2.axis([tmin,tmax,0,5.2]) 160 | 161 | pl.legend((pl.Line2D([0],[0],dashes=[2,2],color='k'),pl.Line2D([0],[0],linestyle='--',color='k')), 162 | ("$3_{2,1}-2_{2,0}$","$3_{0,3}-2_{0,2}$"), 163 | fontsize=16, 164 | loc='center right', 165 | bbox_to_anchor=[1,1.2]) 166 | 167 | #pl.savefig("pH2CO_321-220_vs_temperature_N=%1.0e_X=%0.1e.pdf" % (nh2,abundance),bbox_inches='tight') 168 | pl.savefig("pH2CO_flux_and_ratio_vs_temperature_N=%1.0e_X=%0.1e.pdf" % (nh2,abundance),bbox_inches='tight') 169 | -------------------------------------------------------------------------------- /examples/oh2co_density_grid.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyradex 3 | import pyradex.fjdu 4 | import pylab as pl 5 | import numpy as np 6 | import matplotlib 7 | 8 | # for pretty on-screen plots 9 | if os.path.exists('/Users/adam/.matplotlib/ggplotrc'): 10 | matplotlib.rc_file('/Users/adam/.matplotlib/ggplotrc') 11 | 12 | ndens = 100 13 | 14 | #temperatures = np.linspace(10,50,nabund) 15 | temperature = 50. 16 | densities = np.logspace(1.5,7.,ndens) 17 | abundances = np.array([-10, -9,-8],dtype='float') 18 | opr = 1. # assume an even mix 19 | fortho = opr/(1.+opr) 20 | # background is... not obviously working 21 | #background = 10.0 # instead of 2.73 22 | 23 | nabund = abundances.size 24 | 25 | taugrid_6cm = np.empty([ndens,nabund]) 26 | texgrid_6cm = np.empty([ndens,nabund]) 27 | tbgrid_6cm = np.empty([ndens,nabund]) 28 | fluxgrid_6cm = np.empty([ndens,nabund]) 29 | 30 | taugrid_2cm = np.empty([ndens,nabund]) 31 | texgrid_2cm = np.empty([ndens,nabund]) 32 | tbgrid_2cm = np.empty([ndens,nabund]) 33 | fluxgrid_2cm = np.empty([ndens,nabund]) 34 | 35 | taugrid_1cm = np.empty([ndens,nabund]) 36 | texgrid_1cm = np.empty([ndens,nabund]) 37 | tbgrid_1cm = np.empty([ndens,nabund]) 38 | fluxgrid_1cm = np.empty([ndens,nabund]) 39 | 40 | columngrid = np.empty([ndens,nabund]) 41 | 42 | if not os.path.exists('oh2co-h2.dat'): 43 | import urllib 44 | urllib.urlretrieve('http://home.strw.leidenuniv.nl/~moldata/datafiles/oh2co-h2.dat') 45 | 46 | Radex = pyradex.fjdu.Fjdu 47 | Radex = pyradex.Radex 48 | 49 | R = Radex(species='oh2co-h2', collider_densities={'H2':1e6}, 50 | column=1e12, temperature=temperature) 51 | R.run_radex() 52 | 53 | # get the table so we can look at the frequency grid 54 | table = R.get_table() 55 | 56 | # Target frequencies: 57 | table[np.array([0,2,5])].pprint() 58 | 59 | R.temperature = temperature 60 | for ii,aa in enumerate(10.**abundances): 61 | for jj,dd in enumerate(densities): 62 | R.density = {'oH2':dd*fortho,'pH2':dd*(1-fortho)} 63 | R.abundance = aa 64 | R.run_radex(reuse_last=False, reload_molfile=True) 65 | 66 | TI = R.source_line_brightness_temperature 67 | taugrid_6cm[jj,ii] = R.tau[0] 68 | texgrid_6cm[jj,ii] = R.tex[0].value 69 | tbgrid_6cm[jj,ii] = R.T_B[0].value 70 | fluxgrid_6cm[jj,ii] = TI[0].value 71 | 72 | taugrid_2cm[jj,ii] = R.tau[2] 73 | texgrid_2cm[jj,ii] = R.tex[2].value 74 | tbgrid_2cm[jj,ii] = R.T_B[2].value 75 | fluxgrid_2cm[jj,ii] = TI[2].value 76 | 77 | taugrid_1cm[jj,ii] = R.tau[5] 78 | texgrid_1cm[jj,ii] = R.tex[5].value 79 | tbgrid_1cm[jj,ii] = R.T_B[5].value 80 | fluxgrid_1cm[jj,ii] = TI[5].value 81 | 82 | columngrid[jj,ii] = R.column.value 83 | 84 | 85 | pl.figure(1) 86 | pl.clf() 87 | ax1 = pl.subplot(2,1,1) 88 | ax1.loglog(densities,taugrid_6cm[:,0],label='$X=10^{%i}$' % abundances[0]) 89 | ax1.loglog(densities,taugrid_2cm[:,0]) 90 | ax1.loglog(densities,taugrid_6cm[:,1],linestyle='--',label='$X=10^{%i}$' % abundances[1]) 91 | ax1.loglog(densities,taugrid_2cm[:,1],linestyle='--') 92 | ax1.loglog(densities,taugrid_6cm[:,2],linestyle=':',label='$X=10^{%i}$' % abundances[2]) 93 | ax1.loglog(densities,taugrid_2cm[:,2],linestyle=':') 94 | ax1.set_xticks([]) 95 | ax1.set_ylabel("$\\tau$") 96 | pl.legend(loc='best',fontsize=14) 97 | ax2 = pl.subplot(2,1,2) 98 | ax2.semilogx(densities,taugrid_6cm[:,0]/taugrid_2cm[:,0]) 99 | ax2.semilogx(densities,taugrid_6cm[:,1]/taugrid_2cm[:,1],linestyle='--') 100 | ax2.set_xlabel("log n(H$_2$) [cm$^{-3}$]") 101 | ax2.set_ylabel("Ratio") 102 | pl.subplots_adjust(hspace=0) 103 | pl.show() 104 | 105 | pl.figure(2) 106 | pl.clf() 107 | ax1 = pl.subplot(2,1,1) 108 | ax1.semilogx(densities,texgrid_6cm[:,0],label='$X=10^{%i}$' % abundances[0]) 109 | ax1.semilogx(densities,texgrid_2cm[:,0]) 110 | ax1.semilogx(densities,texgrid_6cm[:,1],linestyle='--',label='$X=10^{%i}$' % abundances[1]) 111 | ax1.semilogx(densities,texgrid_2cm[:,1],linestyle='--') 112 | ax1.semilogx(densities,texgrid_6cm[:,2],linestyle=':',label='$X=10^{%i}$' % abundances[2]) 113 | ax1.semilogx(densities,texgrid_2cm[:,2],linestyle=':') 114 | ax1.set_xticks([]) 115 | ax1.set_ylabel("$T_{ex}$") 116 | ax1.hlines(2.73,10,1e7,color='k', linewidth=3, alpha=0.3, zorder=-1) 117 | ax1.set_ylim(0,4) 118 | ax1.set_xlim(densities.min(),densities.max()) 119 | pl.legend(loc='best', fontsize=14) 120 | ax2 = pl.subplot(2,1,2) 121 | ax2.semilogx(densities,texgrid_6cm[:,0]/texgrid_2cm[:,0]) 122 | ax2.semilogx(densities,texgrid_6cm[:,1]/texgrid_2cm[:,1],linestyle='--') 123 | ax2.set_xlabel("log n(H$_2$) [cm$^{-3}$]") 124 | ax2.set_ylabel("Ratio") 125 | ax2.set_ylim(0,1) 126 | pl.subplots_adjust(hspace=0) 127 | pl.show() 128 | 129 | pl.figure(3) 130 | pl.clf() 131 | ax1 = pl.subplot(2,1,1) 132 | ax1.semilogx(densities,tbgrid_6cm[:,0],label='$X=10^{%i}$' % abundances[0]) 133 | ax1.semilogx(densities,tbgrid_2cm[:,0]) 134 | ax1.semilogx(densities,tbgrid_6cm[:,1],linestyle='--',label='$X=10^{%i}$' % abundances[1]) 135 | ax1.semilogx(densities,tbgrid_2cm[:,1],linestyle='--') 136 | ax1.semilogx(densities,tbgrid_6cm[:,2],linestyle=':',label='$X=10^{%i}$' % abundances[2]) 137 | ax1.semilogx(densities,tbgrid_2cm[:,2],linestyle=':') 138 | ax1.set_xticks([]) 139 | ax1.set_ylabel("$T_{B}$") 140 | ax1.hlines(2.73,10,1e7,color='k', linewidth=3, alpha=0.3, zorder=-1) 141 | ax1.set_ylim(-3,10) 142 | ax1.set_xlim(densities.min(),densities.max()) 143 | pl.legend(loc='best', fontsize=14) 144 | ax2 = pl.subplot(2,1,2) 145 | ax2.semilogx(densities,tbgrid_6cm[:,0]/tbgrid_2cm[:,0]) 146 | ax2.semilogx(densities,tbgrid_6cm[:,1]/tbgrid_2cm[:,1],linestyle='--') 147 | ax2.set_xlabel("log n(H$_2$) [cm$^{-3}$]") 148 | ax2.set_ylabel("Ratio") 149 | ax2.set_ylim(-1,10) 150 | pl.subplots_adjust(hspace=0) 151 | pl.show() 152 | 153 | 154 | fig4 = pl.figure(4) 155 | fig4.clf() 156 | ax1 = pl.subplot(2,1,1) 157 | cm2, = ax1.loglog(densities,taugrid_2cm[:,0],label='$X=10^{%i}$' % abundances[0]) 158 | cm1, = ax1.loglog(densities,taugrid_1cm[:,0]) 159 | ax1.loglog(densities,taugrid_2cm[:,1],linestyle='--',label='$X=10^{%i}$' % abundances[1], 160 | color=cm2.get_color()) 161 | ax1.loglog(densities,taugrid_1cm[:,1],linestyle='--', 162 | color=cm1.get_color()) 163 | ax1.loglog(densities,taugrid_2cm[:,2],linestyle=':',label='$X=10^{%i}$' % abundances[2], 164 | color=cm2.get_color()) 165 | ax1.loglog(densities,taugrid_1cm[:,2],linestyle=':', 166 | color=cm1.get_color()) 167 | ax1.set_xticks([]) 168 | ax1.set_ylabel("$\\tau$") 169 | ax1.set_ylim(1e-5, 1e2) 170 | pl.legend(loc='best',fontsize=14) 171 | ax2 = pl.subplot(2,1,2) 172 | ax2.semilogx(densities,1./(taugrid_2cm[:,0]/taugrid_1cm[:,0])) 173 | ax2.semilogx(densities,1./(taugrid_2cm[:,1]/taugrid_1cm[:,1]),linestyle='--') 174 | ax2.semilogx(densities,1./(taugrid_2cm[:,2]/taugrid_1cm[:,2]),linestyle=':') 175 | ax2.set_xlabel("log n(H$_2$) [cm$^{-3}$]") 176 | ax2.set_ylabel("Ratio") 177 | pl.subplots_adjust(hspace=0) 178 | pl.show() 179 | 180 | pl.figure(6) 181 | pl.clf() 182 | ax1 = pl.subplot(2,1,1) 183 | ax1.semilogx(densities,tbgrid_2cm[:,0],label='$X=10^{%i}$' % abundances[0]) 184 | ax1.semilogx(densities,tbgrid_1cm[:,0]) 185 | ax1.semilogx(densities,tbgrid_2cm[:,1],linestyle='--',label='$X=10^{%i}$' % abundances[1]) 186 | ax1.semilogx(densities,tbgrid_1cm[:,1],linestyle='--') 187 | ax1.semilogx(densities,tbgrid_2cm[:,2],linestyle=':',label='$X=10^{%i}$' % abundances[2]) 188 | ax1.semilogx(densities,tbgrid_1cm[:,2],linestyle=':') 189 | ax1.set_xticks([]) 190 | ax1.set_ylabel("$T_{B}$") 191 | ax1.hlines(2.73,10,1e7,color='k', linewidth=3, alpha=0.3, zorder=-1) 192 | ax1.set_ylim(-3,10) 193 | ax1.set_xlim(densities.min(),densities.max()) 194 | pl.legend(loc='best', fontsize=14) 195 | ax2 = pl.subplot(2,1,2) 196 | ax2.semilogx(densities,tbgrid_2cm[:,0]/tbgrid_1cm[:,0]) 197 | ax2.semilogx(densities,tbgrid_2cm[:,1]/tbgrid_1cm[:,1],linestyle='--') 198 | ax2.semilogx(densities,tbgrid_2cm[:,2]/tbgrid_1cm[:,2],linestyle=':') 199 | ax2.set_xlabel("log n(H$_2$) [cm$^{-3}$]") 200 | ax2.set_ylabel("Ratio") 201 | ax2.set_ylim(-1,10) 202 | pl.subplots_adjust(hspace=0) 203 | pl.show() 204 | -------------------------------------------------------------------------------- /pyradex/synthspec.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tools to generate synthetic spectra given a table of line strengths 3 | """ 4 | import numpy as np 5 | 6 | from astropy.modeling import models 7 | from astropy import units as u 8 | from astropy import constants as c 9 | 10 | 11 | class SyntheticSpectrum(object): 12 | """ 13 | Synthetic Spectrum class - neato! 14 | """ 15 | 16 | def __init__(self, wcs, species, linewidth): 17 | self.wcs = wcs 18 | self.species = species 19 | self.linewidth = linewidth 20 | 21 | @classmethod 22 | def from_table(cls, wcs, table, species, 23 | linewidth=1.0*u.km/u.s, 24 | profile_function=models.Gaussian1D): 25 | """ 26 | Create a synthetic spectrum from a RADEX (or DESPOTIC, eventually) 27 | output 28 | 29 | Parameters 30 | ---------- 31 | wcs: SpectralWCS 32 | A spectral world coordinate system. You can generate one with 33 | FrequencyArray or specutils.wcs.Spectrum1DLookupWCS 34 | table: astropy.Table 35 | Result of the RADEX query (from R.get_table()) 36 | linewidth: u.Quantity (km/s) 37 | The width of the line to plot 38 | npts: int 39 | The number of spectral points to include 40 | profile_function: astropy.modeling.model 41 | The model function to use. Must accept, in order: 42 | 43 | * flux (peak) 44 | * frequency center (Hz) 45 | * frequency width (Hz) 46 | 47 | 48 | Examples 49 | -------- 50 | >>> from pyradex import Radex,synthspec 51 | >>> R = Radex(species='ch3cn', column=1e14, density=1e5, collider_densities=None) 52 | >>> R.run_radex() 53 | >>> wcs = synthspec.FrequencyArray(91.95*u.GHz, 92*u.GHz, npts=1000) 54 | >>> S = synthspec.SyntheticSpectrum.from_table(wcs, R.get_table(), 55 | ... species='ch3cn') 56 | >>> S.plot() 57 | """ 58 | 59 | self = cls(wcs, species, linewidth) 60 | 61 | self.profile_function = profile_function 62 | 63 | if hasattr(wcs,'minfreq'): 64 | self.minfreq,self.maxfreq = wcs.minfreq,wcs.maxfreq 65 | else: 66 | self.minfreq,self.maxfreq = wcs.min(),wcs.max() 67 | 68 | linefreqs = u.Quantity(table['frequency'], 69 | unit=u.Unit(table['frequency'].unit)) 70 | self.table = table[(linefreqs>self.minfreq) & (linefreqs>> from pyradex import Radex,synthspec 113 | >>> R = Radex(species='ch3cn') 114 | >>> R.run_radex() 115 | >>> wcs = synthspec.FrequencyArray(91.95*u.GHz, 92*u.GHz, npts=1000) 116 | >>> S = synthspec.SyntheticSpectrum.from_RADEX(wcs, R) 117 | >>> S.plot() 118 | """ 119 | 120 | self = cls(wcs, rad.species, linewidth) 121 | 122 | self.profile_function = profile_function 123 | 124 | self.wcs = wcs 125 | if hasattr(wcs,'minfreq'): 126 | self.minfreq,self.maxfreq = wcs.minfreq,wcs.maxfreq 127 | else: 128 | self.minfreq,self.maxfreq = wcs.min(),wcs.max() 129 | 130 | self.rad = rad 131 | linefreqs = rad.frequency 132 | linefreq_mask = (linefreqs>self.minfreq) & (linefreqs>> from pyradex import Radex,synthspec 202 | >>> radex_pars = dict(temperature=20, column=1e13, 203 | ... abundance=10**-8.5, 204 | ... collider_densities={'H2':1e4}) 205 | >>> R = Radex(species='oh2co-h2', **radex_pars) 206 | >>> R.run_radex() 207 | >>> wcs = synthspec.FrequencyArray(4.828*u.GHz, 4.830*u.GHz, npts=1000) 208 | >>> S = synthspec.SyntheticSpectrum.from_RADEX(wcs, R) 209 | >>> S.plot() 210 | >>> radex_pars['temperature'] = 50 211 | >>> S2 = S(velocity_offset=2*u.km/u.s, **radex_pars) 212 | >>> S2.plot() 213 | """ 214 | 215 | from .core import Radex 216 | 217 | rad = Radex(species=self.species, **kwargs) 218 | 219 | if linewidth is None: 220 | linewidth = self.linewidth 221 | else: 222 | self.linewidth = linewidth 223 | 224 | self.rad = rad 225 | linefreqs = rad.frequency 226 | linefreq_mask = (linefreqs>self.minfreq) & (linefreqs 218) & 37 | (table['frequency'] < 220))[0] 38 | key_321 = np.where((table['upperlevel'] == '3_2_1') & 39 | (table['frequency'] > 218) & 40 | (table['frequency'] < 220))[0] 41 | key_322 = np.where((table['upperlevel'] == '3_2_2') & 42 | (table['frequency'] > 218) & 43 | (table['frequency'] < 220))[0] 44 | 45 | 46 | def compute_grid(densities=densities, temperatures=temperatures, fortho=fortho, 47 | columnperbin=abundance*1e23, deltav=1.0, escapeProbGeom='lvg', 48 | R=R): 49 | 50 | R.escapeProbGeom=escapeProbGeom 51 | 52 | ndens = len(densities) 53 | ntemp = len(temperatures) 54 | 55 | taugrid_303 = np.empty([ndens,ntemp]) 56 | texgrid_303 = np.empty([ndens,ntemp]) 57 | fluxgrid_303 = np.empty([ndens,ntemp]) 58 | taugrid_321 = np.empty([ndens,ntemp]) 59 | texgrid_321 = np.empty([ndens,ntemp]) 60 | fluxgrid_321 = np.empty([ndens,ntemp]) 61 | taugrid_322 = np.empty([ndens,ntemp]) 62 | texgrid_322 = np.empty([ndens,ntemp]) 63 | fluxgrid_322 = np.empty([ndens,ntemp]) 64 | columngrid = np.empty([ndens,ntemp]) 65 | 66 | for ii,tt in enumerate(temperatures): 67 | R.temperature = tt 68 | for jj,dd in enumerate(densities): 69 | R.density = {'oH2':dd*fortho,'pH2':dd*(1-fortho)} 70 | #R.abundance = abundance # reset column to the appropriate value 71 | R.column_per_bin = columnperbin 72 | R.deltav = deltav 73 | R.run_radex(reuse_last=True, reload_molfile=True, 74 | validate_colliders=False) 75 | 76 | TI = R.source_line_surfbrightness 77 | taugrid_303[jj,ii] = R.tau[key_303] 78 | texgrid_303[jj,ii] = R.tex[key_303].value 79 | fluxgrid_303[jj,ii] = TI[key_303].value 80 | taugrid_321[jj,ii] = R.tau[key_321] 81 | texgrid_321[jj,ii] = R.tex[key_321].value 82 | fluxgrid_321[jj,ii] = TI[key_321].value 83 | taugrid_322[jj,ii] = R.tau[key_322] 84 | texgrid_322[jj,ii] = R.tex[key_322].value 85 | fluxgrid_322[jj,ii] = TI[key_322].value 86 | columngrid[jj,ii] = R.column.value 87 | 88 | return (TI, 89 | taugrid_303,texgrid_303,fluxgrid_303, 90 | taugrid_321,texgrid_321,fluxgrid_321, 91 | taugrid_322,texgrid_322,fluxgrid_322, 92 | columngrid) 93 | (TI, taugrid_303,texgrid_303,fluxgrid_303, 94 | taugrid_321,texgrid_321,fluxgrid_321, 95 | taugrid_322,texgrid_322,fluxgrid_322, 96 | columngrid) = compute_grid() 97 | 98 | if __name__ == "__main__": 99 | import pylab as pl 100 | from matplotlib.ticker import FuncFormatter 101 | import matplotlib 102 | 103 | 104 | johnstonpars = [ 105 | (1e13, 10, ), 106 | (1e14, 5, ), 107 | (1e14, 10, ), 108 | (1e14, 20, ), 109 | (1e15, 10, ), 110 | ] 111 | 112 | for geom in ('lvg','sphere','slab'): 113 | for opr in (0.01, 3): 114 | for colh2co, dv in johnstonpars: 115 | (TI, taugrid_303,texgrid_303,fluxgrid_303, 116 | taugrid_321,texgrid_321,fluxgrid_321, 117 | taugrid_322,texgrid_322,fluxgrid_322, 118 | columngrid) = compute_grid(columnperbin=colh2co, deltav=dv, escapeProbGeom=geom) 119 | 120 | fig = pl.figure(1) 121 | fig.clf() 122 | ax = fig.gca() 123 | 124 | im = ax.imshow((fluxgrid_303/fluxgrid_321).T, vmin=1.0, vmax=10, aspect=fluxgrid_303.shape[0]/float(fluxgrid_303.shape[1]), 125 | norm=matplotlib.colors.LogNorm(), cmap='cubehelix') 126 | ax.plot(densities,[300]*len(densities), 'r--', linewidth=2, alpha=0.5) 127 | cb = fig.colorbar(im) 128 | cb.set_ticks([1.3,1.6]+range(1,11)) 129 | cb.set_ticklabels([1.3,1.6]+range(1,11)) 130 | c = ax.contour((fluxgrid_303/fluxgrid_321).T, levels=[1.4,1.5,1.6,1.7,1.8], colors=['w']*3) 131 | ax.clabel(c, inline=1, fontsize=10, color='w') 132 | ax.xaxis.set_major_formatter(FuncFormatter(lambda x,y: "{0:0.2f}".format(np.log10(densities[int(x)])) if x1 collider implies 171 | oH2+pH2, which is hard-coded into readdata.f and is incorrect. 172 | """ 173 | rdx = Radex(species='hcn', collider_densities={'H2':1e4, 'e': 1e2}, 174 | column_per_bin=1e14, deltav=1.0, temperature=30, 175 | tbackground=2.73) 176 | result_table = rdx() 177 | 178 | 179 | def test_mod_params(): 180 | 181 | RR = Radex(datapath='examples/', species='co', column=1e15, 182 | density=1e3, temperature=20) 183 | 184 | tbl = RR() 185 | 186 | np.testing.assert_almost_equal(tbl[0]['Tex'], 8.69274406690759, decimal=2) 187 | 188 | RR.column = 1e14 189 | tbl = RR() 190 | 191 | np.testing.assert_almost_equal(tbl[0]['Tex'], 8.0986662583317646, decimal=2) 192 | 193 | RR.density=1e4 194 | tbl = RR() 195 | np.testing.assert_almost_equal(tbl[0]['Tex'], 25.381267019506591, decimal=1) 196 | 197 | RR.temperature=25 198 | tbl = RR() 199 | np.testing.assert_almost_equal(tbl[0]['Tex'], 37.88, decimal=1) 200 | 201 | RR.deltav = 5 * u.km/u.s 202 | np.testing.assert_almost_equal(RR.deltav.to(u.km/u.s).value, 5) 203 | tbl = RR() 204 | np.testing.assert_almost_equal(tbl[0]['Tex'], 37.83, decimal=1) 205 | 206 | 207 | if __name__ == "__main__": 208 | test_call() 209 | test_parse_example() 210 | for mol in ['co','13co','c18o','o-h2co','p-nh3']: 211 | test_molecules(mol) 212 | -------------------------------------------------------------------------------- /pyradex/despotic_interface.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import warnings 3 | import os 4 | from collections import defaultdict 5 | from .utils import united,uvalue 6 | 7 | from astropy import units as u 8 | from astropy import constants 9 | from astropy.table import Table,Column 10 | 11 | class Despotic(object): 12 | """ 13 | A class meant to be similar to RADEX in terms of how it is called but to 14 | use Despotic instead of RADEX as the backend 15 | """ 16 | 17 | # escapeprobabilty geometry names 18 | _epgdict = {'lvg':'LVG','sphere':'sphere','slab':'slab'} 19 | 20 | def __call__(self, **kwargs): 21 | # reset the parameters appropriately 22 | self.__init__(**kwargs) 23 | return self.lineLum() 24 | 25 | def __init__(self, 26 | collider_densities={'ph2':990,'oh2':10}, 27 | temperature=30, 28 | species='co', 29 | datapath=None, 30 | hcolumn=1e21, 31 | abundance=1e-5, 32 | #column=1e13, 33 | tbackground=2.7315, 34 | deltav=1.0, 35 | escapeProbGeom='lvg', 36 | outfile='radex.out', 37 | logfile='radex.log', 38 | debug=False, 39 | ): 40 | """ 41 | Interface to DESPOTIC 42 | 43 | Parameters 44 | ---------- 45 | collider_densities: dict 46 | Dictionary giving the volume densities of the collider(s) in units of 47 | cm^-3. Valid entries are h2,oh2,ph2,e,He,H,H+. The keys are 48 | case-insensitive. 49 | temperature: float 50 | Local gas temperature in K 51 | species: str 52 | A string specifying a valid chemical species. This is used to look 53 | up the specified molecule 54 | hcolumn: float 55 | The total column density of hydrogen. 56 | abundance: float 57 | The molecule's abundance relative to H (NOT H2 as is normally done!). 58 | tbackground: float 59 | Background radiation temperature (e.g., CMB) 60 | deltav: float 61 | The FWHM line width (really, the single-zone velocity width to 62 | scale the column density by: this is most sensibly interpreted as a 63 | velocity gradient (dv/dR)) 64 | sigmaNT: float 65 | Nonthermal velocity dispersion 66 | (this is strictly ignored - deltav IS sigmant) 67 | datapath: str 68 | Path to the molecular data files 69 | """ 70 | 71 | import despotic 72 | 73 | self.cloud = despotic.cloud() 74 | self.cloud.nH = float(np.sum([collider_densities[k]*2 if 'h2' in k.lower() 75 | else collider_densities[k] 76 | for k in collider_densities])) 77 | 78 | for k in collider_densities.keys(): 79 | collider_densities[k.lower()] = collider_densities[k] 80 | 81 | if 'ph2' in collider_densities: 82 | self.cloud.comp.xpH2 = collider_densities['ph2'] / self.cloud.nH 83 | if 'oh2' in collider_densities: 84 | self.cloud.comp.xoH2 = collider_densities['oh2'] / self.cloud.nH 85 | 86 | self.cloud.Td = uvalue(temperature,u.K) 87 | self.cloud.Tg = uvalue(temperature,u.K) 88 | self.cloud.dust.sigma10 = 0.0 89 | 90 | self.cloud.colDen = uvalue(hcolumn,u.cm**-2) 91 | 92 | 93 | if uvalue(tbackground,u.K) > 2.7315: 94 | self.cloud.rad.TradDust = uvalue(tbackground,u.K) 95 | 96 | self.species = species 97 | if datapath is None: 98 | emitterFile = species+'.dat' 99 | else: 100 | emitterFile = os.path.expanduser(os.path.join(datapath, species+'.dat')) 101 | self.cloud.addEmitter(species, abundance, emitterFile=emitterFile) 102 | 103 | self.cloud.comp.computeDerived(self.cloud.nH) 104 | 105 | self.escapeProbGeom = escapeProbGeom 106 | 107 | self.deltav = deltav 108 | 109 | 110 | def recompute(self): 111 | self.cloud.comp.computeDerived(self.cloud.nH) 112 | self.lineLum() 113 | 114 | @property 115 | def deltav(self): 116 | if self.cloud.sigmaNT > 0: 117 | return (self.cloud.sigmaNT*u.cm/u.s).to(u.km/u.s) 118 | elif self.cloud.dVdr > 0: 119 | return (self.cloud.dVdr*u.s**-1).to(u.km/u.s/u.pc) 120 | else: 121 | raise ValueError("The velocity gradient is zero") 122 | 123 | @deltav.setter 124 | def deltav(self, deltav): 125 | 126 | if self.escapeProbGeom == 'LVG': 127 | self._dv = united(self.cloud.dVdr,u.s**-1) 128 | # See notes.rst: DESPOTIC must have a different dVdR to get the 129 | # same results as RADEX 130 | # 1.0645 / sqrt(8*log(2)) = sqrt(2 * pi) / (8*log(2)) 131 | self.cloud.dVdr = (uvalue(united(deltav,u.km/u.s/u.pc),u.s**-1) * 132 | np.sqrt(8*np.log(2))*2) 133 | # (2*np.pi)**0.5/(8*np.log(2)) 134 | 135 | else: 136 | self._dv = united(self.cloud.sigmaNT,u.km/u.s) 137 | 138 | FWHM = united(deltav,u.km/u.s) 139 | self.sigmaTot = FWHM/np.sqrt(8.0*np.log(2)) 140 | self.cloud.sigmaNT = uvalue(np.sqrt(self.sigmaTot**2 - 141 | self.cs**2/self.cloud.emitters[self.species].data.molWgt), 142 | u.km/u.s) 143 | 144 | @property 145 | def cs(self): 146 | return np.sqrt(constants.k_B*united(self.cloud.Tg,u.K)/(self.cloud.comp.mu*constants.m_p)).to(u.km/u.s) 147 | 148 | def lineLum(self, **kwargs): 149 | if 'escapeProbGeom' not in kwargs: 150 | kwargs['escapeProbGeom'] = self.escapeProbGeom 151 | return self.cloud.lineLum(self.species, **kwargs) 152 | 153 | @property 154 | def escapeProbGeom(self): 155 | return self._epg 156 | 157 | @escapeProbGeom.setter 158 | def escapeProbGeom(self, escapeProbGeom): 159 | mdict = self._epgdict 160 | if escapeProbGeom.lower() not in mdict: 161 | raise ValueError("Invalid escapeProbGeom, must be one of "+",".join(mdict.values())) 162 | self._epg = mdict[escapeProbGeom] 163 | 164 | @property 165 | def density(self): 166 | d = {'H2':self.cloud.comp.xH2*self.nH, 167 | 'oH2':self.cloud.comp.xoH2*self.nH, 168 | 'pH2':self.cloud.comp.xpH2*self.nH, 169 | 'e':self.cloud.comp.xe*self.nH, 170 | 'H':self.cloud.comp.xHI*self.nH, 171 | 'He':self.cloud.comp.xHe*self.nH, 172 | 'H+':self.cloud.comp.xHplus*self.nH} 173 | if d['H2'] != 0 and (d['oH2'] != 0 or d['pH2'] != 0): 174 | # either ortho + para or total H2, not both 175 | d['H2'] = 0 176 | if u: 177 | for k in d: 178 | d[k] = d[k] * u.cm**-3 179 | return d 180 | 181 | @property 182 | def nH(self): 183 | return self.cloud.nH 184 | 185 | @nH.setter 186 | def nH(self, nh): 187 | self.cloud.nH = nh 188 | 189 | @property 190 | def nH2(self): 191 | return self.cloud.nH/2. 192 | 193 | @nH2.setter 194 | def nH2(self, nh2): 195 | self.nh = nh2*2. 196 | 197 | @property 198 | def beta(self): 199 | emitter = self.cloud.emitters[self.species] 200 | ep = [emitter.escapeProb[upper,lower] 201 | for (upper,lower) in zip(emitter.data.radUpper, 202 | emitter.data.radLower)] 203 | return ep 204 | 205 | @density.setter 206 | def density(self, collider_density): 207 | collider_densities = defaultdict(lambda: 0) 208 | for k in collider_density: 209 | collider_densities[k.upper()] = collider_density[k] 210 | 211 | if 'OH2' in collider_densities: 212 | if not 'PH2' in collider_densities: 213 | raise ValueError("If o-H2 density is specified, p-H2 must also be.") 214 | collider_densities['H2'] = (collider_densities['OH2'] + collider_densities['PH2']) 215 | elif 'H2' in collider_densities: 216 | warnings.warn("Using a default ortho-to-para ratio (which " 217 | "will only affect species for which independent " 218 | "ortho & para collision rates are given)") 219 | 220 | T = self.temperature.value if hasattr(self.temperature,'value') else self.temperature 221 | if T > 0: 222 | opr = min(3.0,9.0*np.exp(-170.6/T)) 223 | else: 224 | opr = 3.0 225 | fortho = opr/(1+opr) 226 | collider_densities['OH2'] = collider_densities['H2']*fortho 227 | collider_densities['PH2'] = collider_densities['H2']*(1-fortho) 228 | 229 | total_density = np.sum([collider_densities[x] * (2 if '2' in x else 1) 230 | for x in (['OH2','PH2','H','E','HE','H+'])]) 231 | self.nH = total_density 232 | 233 | self.cloud.comp.xH2 = collider_densities['H2']/self.nH 234 | self.cloud.comp.xoH2 = (collider_densities['OH2'])/self.nH 235 | self.cloud.comp.xpH2 = (collider_densities['PH2'])/self.nH 236 | 237 | self.cloud.comp.xe = collider_densities['E']/self.nH 238 | self.cloud.comp.xHI = collider_densities['H']/self.nH 239 | self.cloud.comp.xHe = collider_densities['HE']/self.nH 240 | self.cloud.comp.xHplus = collider_densities['H+']/self.nH 241 | 242 | self.cloud.comp.computeDerived() 243 | 244 | @property 245 | def temperature(self): 246 | return self.cloud.Tg 247 | 248 | @property 249 | def upperlevelpop(self): 250 | return self.cloud.emitters[self.species].levPop[1:] 251 | 252 | @property 253 | def lowerlevelpop(self): 254 | return self.cloud.emitters[self.species].levPop[:-1] 255 | 256 | def get_table(self, **kwargs): 257 | 258 | D = self.lineLum(**kwargs) 259 | 260 | # remap names to match RADEX 261 | name_mapping = {'upper':'upperlevel', 262 | 'lower':'lowerlevel', 263 | 'freq':'frequency',} 264 | 265 | 266 | names = D[0].keys() 267 | T = Table(names=[name_mapping[n] 268 | if n in name_mapping 269 | else n 270 | for n in names], 271 | dtype=[type(D[0][k]) for k in names]) 272 | 273 | for row in D: 274 | T.add_row([row[k] for k in names]) 275 | 276 | T.add_column(Column(name='upperlevelpop', 277 | data=self.upperlevelpop, 278 | dtype='float')) 279 | T.add_column(Column(name='lowerlevelpop', 280 | data=self.lowerlevelpop, 281 | dtype='float')) 282 | 283 | return T 284 | -------------------------------------------------------------------------------- /install_radex.py: -------------------------------------------------------------------------------- 1 | #raise "This was left in a terrible debug state may 25, 2021" 2 | from __future__ import print_function 3 | import tarfile 4 | import sys 5 | import re 6 | import os 7 | import shutil 8 | import glob 9 | import warnings 10 | from numpy import f2py 11 | import subprocess 12 | from subprocess import CompletedProcess 13 | try: 14 | from astropy.utils.data import download_file 15 | except ImportError: 16 | download_file = None 17 | 18 | 19 | def install_radex(download=True, extract=True, patch=True, compile=True): 20 | if download: 21 | filename = download_radex() 22 | if extract: 23 | extract_radex(filename) 24 | if patch: 25 | patch_radex() 26 | if compile: 27 | compile_radex() 28 | 29 | def download_radex(redownload=True, 30 | url='https://personal.sron.nl/~vdtak/radex/radex_public.tar.gz'): 31 | 32 | filename = 'radex_public.tar.gz' 33 | 34 | if os.path.isfile(filename) and not redownload: 35 | return filename 36 | 37 | print("Downloading RADEX") 38 | 39 | try: 40 | filename = download_file(url, cache=True) 41 | assert os.path.exists(filename) 42 | except: 43 | import requests 44 | r = requests.get(url, 45 | #data={'filename':filename}, 46 | stream=True, 47 | verify=False) 48 | with open(filename,'wb') as f: 49 | for chunk in r.iter_content(1024): 50 | f.write(chunk) 51 | 52 | print("Download succeeded, or at least didn't obviously fail.") 53 | 54 | return filename 55 | 56 | def extract_radex(filename='radex_public.tar.gz'): 57 | print("Extracting RADEX source from file {0}".format(filename)) 58 | with tarfile.open(filename,mode='r:gz') as tar: 59 | # this will fail if readonly files are overwritten: tf.extractall() 60 | for file_ in tar: 61 | try: 62 | tar.extract(file_) 63 | except IOError as e: 64 | os.remove(file_.name) 65 | tar.extract(file_) 66 | finally: 67 | os.chmod(file_.name, file_.mode) 68 | 69 | def patch_radex(): 70 | # PATCHES 71 | radlines = [] 72 | vers = re.compile("parameter\\(version = '([a-z0-9]*)'\)") 73 | with open('Radex/src/radex.inc') as f: 74 | for line in f.readlines(): 75 | if ('parameter(radat' in line or 76 | 'parameter(version' in line or 77 | 'parameter(logfile' in line or 78 | 'parameter (method' in line): 79 | line = 'c'+line 80 | if vers.search(line): 81 | radlines.append("c version = '%s'\n" % vers.search(line).groups()[0]) 82 | if 'parameter(debug' in line: 83 | line = 'c'+line 84 | radlines.append(' common/dbg/debug\n') 85 | radlines.append(line) 86 | 87 | if 'parameter (method = 3)' in line: 88 | radlines.append(' common/setup/radat,method,version,logfile\n') 89 | 90 | with open('Radex/src/radex.inc','w') as f: 91 | for line in radlines: 92 | f.write(line) 93 | 94 | with open('Radex/src/background.f') as f: 95 | lines = f.readlines() 96 | 97 | with open('Radex/src/background.f','w') as f: 98 | for line in lines: 99 | if 'parameter(huge' in line: 100 | f.write(' parameter(huge=1.0e38)\n') 101 | f.write('c ! highest allowed by f90 (fdvt 28apr06)\n') 102 | else: 103 | f.write(line) 104 | 105 | with open('Radex/src/readdata.f') as f: 106 | lines = f.readlines() 107 | 108 | with open('Radex/src/readdata.f','w') as f: 109 | # comment out the block dealing with ortho/para ratio: let python 110 | # handle that 111 | for ii,line in enumerate(lines): 112 | if ii <= 225 or ii > 235: 113 | f.write(line) 114 | else: 115 | f.write("c"+line[1:]) 116 | #if 'density(3) = density(1)/(1.d0+1.d0/opr)' in line: 117 | # f.write(line) 118 | # f.write('c For conservation of total density, set n(H2) = 0\n') 119 | # f.write(' density(1) = 0.0\n') 120 | #else: 121 | # f.write(line) 122 | 123 | 124 | 125 | """ 126 | Works for hpc: 127 | PATH=/Users/adam/repos/hpc/bin/:/usr/bin:~/virtual-python/bin/:/bin FFLAGS='-m64 -fPIC' CFLAGS='-fno-strict-aliasing -fno-automatic -fno-common -dynamic -m64 -g -O2' LDFLAGS='-m64 -undefined dynamic_lookup -bundle' python -c "import install_radex; install_radex.compile_radex(f77exec='/Users/adam/repos/hpc/bin/gfortran')" 128 | 129 | Works for 4.2.3: 130 | FFLAGS='-arch i686 -arch x86_64 -fPIC' CFLAGS='-fno-strict-aliasing -fno-common -dynamic -arch i386 -arch x86_64 -g -O2' LDFLAGS='-arch i686 -arch x86_64 -undefined dynamic_lookup -bundle' python setup.py install_radex 131 | """ 132 | 133 | def compile_radex(fcompiler='gfortran',f77exec=None): 134 | #r1 = os.system('f2py -h pyradex/radex/radex.pyf Radex/src/*.f --overwrite-signature > radex_build.log') 135 | pwd = os.getcwd() 136 | os.chdir('Radex/src/') 137 | files = glob.glob('*.f') 138 | include_path = '--include-paths {0}'.format(os.getcwd()) 139 | f2py.run_main(' -h radex.pyf --overwrite-signature'.split()+include_path.split()+files) 140 | 141 | if f77exec is None: 142 | f77exec='' 143 | else: 144 | f77exec = '--f77exec=%s' % f77exec 145 | #cmd = '-m radex -c %s --fcompiler=%s %s' % (" ".join(files), fcompiler, f77exec) 146 | #f2py.run_main(['-m','radex','-c','--fcompiler={0}'.format(fcompiler), f77exec,] + files) 147 | source_list = [] 148 | for fn in files: 149 | with open(fn, 'r') as f: 150 | source_list.append(f.read()) 151 | source = "\n".join(source_list) 152 | 153 | #with open("merged_source.f", 'wb') as fh: 154 | # fh.write(source) 155 | 156 | include_path = '-I{0}'.format(os.getcwd()) 157 | 158 | import platform 159 | mac_ver = platform.mac_ver() 160 | 161 | # not sure if this check is robust enough 162 | if mac_ver[0] and int(mac_ver[0].split('.')[0]) >= 12: 163 | linkdir = '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib' 164 | if os.path.exists(linkdir): 165 | linker_path = f'-L{linkdir}' 166 | else: 167 | other_linkpaths = glob.glob('/Library/Developer/CommandLineTools/SDKs/MacOSX*.sdk/usr/lib') 168 | if len(other_linkpaths) >= 1: 169 | linker_path = f'-L{other_linkpaths[0]}' 170 | print(f"Set linkpath to {linker_path}") 171 | else: 172 | warnings.warn("NO LINK PATH WAS FOUND! Check that Mac OS X software development kit (SDK) is installed, and check where it's installed; it should be in /Library/Developer/CommandLineTools/SDKs/") 173 | else: 174 | linker_path = '' 175 | 176 | # Check Python version to determine how to specify the compiler 177 | is_py312_or_later = sys.version_info >= (3, 12) 178 | 179 | if is_py312_or_later: 180 | # For Python 3.12+, we need to use FC environment variable instead of --fcompiler 181 | print(f"Python 3.12+ detected, using FC environment variable for compiler: {fcompiler}") 182 | env = os.environ.copy() 183 | env['FC'] = fcompiler 184 | 185 | # Set flags without the --fcompiler option 186 | extra_args = f'--f77flags="-fPIC" {f77exec} {include_path} {linker_path}' 187 | else: 188 | # For older Python versions, use --fcompiler as before 189 | extra_args = f'--f77flags="-fPIC -fno-automatic" --fcompiler={fcompiler} {f77exec} {include_path} {linker_path}' 190 | 191 | print(f"Running f2py with fcompiler={fcompiler}, f77exec={f77exec}, include_path={include_path}, linker_path={linker_path}") 192 | print(f"extra args = {extra_args}") 193 | print(f"Current directory = {os.getcwd()}") 194 | 195 | #f2py_path = os.path.join(sys.exec_prefix, 'bin', 'f2py') 196 | 197 | # Hack doesn't work. 198 | lsrslt = subprocess.run(["ls *.f"], cwd=os.getcwd(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, shell=True) 199 | lsout = lsrslt.stdout.replace('\n', ' ') 200 | print(f".f files are: {lsout} = {glob.glob('*.f')}") 201 | 202 | # For Python 3.12+ with meson, we need to explicitly list all Fortran files 203 | if is_py312_or_later: 204 | # Get the list of .f files 205 | fortran_files = glob.glob("*.f") 206 | 207 | # Build the command as a list of arguments 208 | cmd_args = ['f2py', '-c', '-m', 'radex'] 209 | cmd_args.extend(extra_args.split()) 210 | cmd_args.extend(fortran_files) 211 | command = ' '.join(cmd_args) 212 | 213 | print(f"Running command in {os.getcwd()}: {' '.join(cmd_args)}") 214 | 215 | # Use the environment with FC set for Python 3.12+ 216 | r2 = subprocess.run(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 217 | cwd=os.getcwd(), text=True, env=env) 218 | else: 219 | # For older Python versions, we can use the wildcard 220 | command = f'f2py -c -m radex {extra_args} *.f' 221 | print(f"Running command in {os.getcwd()}: {command}") 222 | 223 | # Use the original environment for older Python versions 224 | r2 = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 225 | cwd=os.getcwd(), text=True, shell=True, env=os.environ) 226 | 227 | print(f"Command completed with return code: {r2.returncode}") 228 | 229 | outfile_ = glob.glob("radex.*so") 230 | if (r2.returncode != 0) or (len(outfile_) != 1): 231 | print("\nCommand failed or no output file was generated. Here's the output:") 232 | print("\n--- STDOUT ---") 233 | print(r2.stdout) 234 | print("\n--- STDERR ---") 235 | print(r2.stderr) 236 | print("\nTry running it manually:") 237 | print(f"cd {os.getcwd()}") 238 | if is_py312_or_later: 239 | print(f"FC={fcompiler} {command}") 240 | else: 241 | print(command) 242 | print("cd -") 243 | print("mv Radex/src/*so pyradex/radex/") 244 | 245 | os.chdir(pwd) 246 | 247 | outfile = glob.glob("Radex/src/radex.*so") 248 | if len(outfile) != 1: 249 | print("outfile = {0}".format(outfile)) 250 | error_msg = "Did not find the correct .so file(s)! Compilation has failed.\n" 251 | error_msg += "Try running the command manually:\n\n" 252 | error_msg += f"cd Radex/src/\n" 253 | 254 | if is_py312_or_later: 255 | error_msg += f"FC={fcompiler} {command}\n" 256 | else: 257 | error_msg += f"{command}\n" 258 | 259 | error_msg += "cd -\n" 260 | error_msg += "mv Radex/src/*so pyradex/radex/\n\n" 261 | error_msg += "See also Github issues 39 and 40" 262 | 263 | raise OSError(error_msg) 264 | sofile = outfile[0] 265 | print(f"Moving {sofile} to pyradex/radex/radex.so") 266 | r3 = shutil.move(sofile, 'pyradex/radex/radex.so') 267 | 268 | def build_radex_executable(datapath='./'): 269 | filename = download_radex(redownload=False) 270 | # need to re-extract the RADEX source to get an un-patched version 271 | extract_radex(filename) 272 | compile_radex_source(datapath=datapath) 273 | 274 | 275 | def compile_radex_source(datapath='./'): 276 | """ 277 | Compile the source file in the Radex/ directory 278 | 279 | May be good to download & untar first 280 | """ 281 | 282 | cwd = os.getcwd() 283 | 284 | # Set datapath to the 'examples' directory if not specified (since there is 285 | # at least co.dat there) 286 | if datapath is None: 287 | datapath = os.path.join(os.getcwd(), 'examples') 288 | print("Datapath was not defined. Set to ",datapath) 289 | 290 | # make sure ~ gets expanded 291 | datapath = os.path.expanduser(datapath) 292 | 293 | try: 294 | os.link('Radex/data/hco+.dat',os.path.join(datapath,'hco+.dat')) 295 | except OSError: 296 | pass 297 | 298 | # I think fortran requires a trailing slash... can't hurt 299 | if datapath[-1] != '/': 300 | datapath = datapath+'/' 301 | 302 | os.chdir('Radex/src/') 303 | with open('radex.inc','r') as f: 304 | lines = [L.replace('/Users/floris/Radex/moldat/',datapath) 305 | if 'radat' in L else L 306 | for L in f.readlines()] 307 | with open('radex.inc','w') as of: 308 | of.writelines(lines) 309 | 310 | method_types = {1:'sphere',2:'lvg',3:'slab'} 311 | for method in method_types: 312 | radex_inc_method('./',method=method) 313 | r1 = os.system('make') 314 | if r1 != 0: 315 | raise SystemError("radex make failed with error %i" % r1) 316 | shutil.move('../bin/radex','../bin/radex_%s' % method_types[method]) 317 | 318 | os.chdir(cwd) 319 | 320 | def radex_inc_method(datapath, method=1): 321 | """ 322 | Convert the radex.inc file to a method 323 | 324 | Parameters 325 | ---------- 326 | datapath: path 327 | a directory path containing the target radex.inc 328 | method: 1,2,3 329 | 1: sphere 330 | 2: lvg 331 | 3: slab 332 | """ 333 | fn = os.path.join(datapath, 'radex.inc') 334 | with open(fn,'r') as f: 335 | lines = [L.replace('/Users/floris/Radex/moldat/',datapath) 336 | if 'radat' in L else L 337 | for L in f.readlines()] 338 | 339 | radlines = [] 340 | for line in lines: 341 | if ('parameter (method' in line): 342 | line = 'c'+line 343 | if 'method = 3' in line: 344 | radlines.append(' parameter (method = %i)\n' % method) 345 | radlines.append(line) 346 | 347 | with open(fn,'w') as f: 348 | f.writelines(radlines) 349 | -------------------------------------------------------------------------------- /pyradex/base_class.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import numpy as np 3 | import astropy.units as u 4 | _quantity = u.Quantity 5 | import os 6 | 7 | from .utils import QuantityOff,ImmutableDict,unitless,grouper 8 | from . import utils 9 | 10 | from astropy import units as u 11 | from astropy import constants 12 | from astropy import log 13 | import astropy.table 14 | 15 | # maybe an ABC? 16 | class RadiativeTransferApproximator(object): 17 | _u_brightness = (u.erg * u.s**-1 * u.cm**-2 * u.Hz**-1 * u.sr**-1) 18 | _u_sc = u.cm**-2 19 | _u_cc = u.cm**-3 20 | 21 | _u_gradient = u.cm**-2 / (u.km/u.s) / u.pc 22 | _u_kms = u.km/u.s 23 | _u_cms = u.cm/u.s 24 | 25 | @property 26 | def locked_parameter(self): 27 | return self._locked_parameter 28 | 29 | def _lock_param(self, parname): 30 | self._locked_parameter = parname 31 | 32 | _all_valid_colliders = {'H2':'H2', 33 | 'PH2':'pH2', 34 | 'OH2':'oH2', 35 | 'E':'e', 36 | 'H':'H', 37 | 'HE':'He', 38 | 'H+':'H+'} 39 | 40 | @property 41 | def density(self): 42 | raise NotImplementedError 43 | 44 | @density.setter 45 | def density(self, collider_density): 46 | raise NotImplementedError 47 | 48 | 49 | @property 50 | def valid_colliders(self): 51 | return self._valid_colliders 52 | 53 | @property 54 | def total_density(self): 55 | """ 56 | The total density *by number of particles* 57 | The *mass density* can be dramatically different! 58 | """ 59 | vc = [x.lower() for x in self.valid_colliders] 60 | if 'h2' in vc: 61 | useh2 = 1 62 | useoph2 = 0 63 | elif 'oh2' in vc or 'ph2' in vc: 64 | useh2 = 0 65 | useoph2 = 1 66 | else: 67 | # Weird case: no H2 colliders at all 68 | useoph2 = 0 69 | useh2 = 0 70 | 71 | weights = {'H2': useh2, 72 | 'PH2': useoph2, 73 | 'OH2': useoph2, 74 | 'E': 1, 75 | 'H': 1, 76 | 'He': 1, 77 | 'H+': 1,} 78 | 79 | return u.Quantity([self.density[k]*weights[k] for k in self.density]).sum() 80 | 81 | 82 | @property 83 | def mass_density(self): 84 | 85 | vc = [x.lower() for x in self.valid_colliders] 86 | if 'h2' in vc: 87 | useh2 = 1 88 | useoph2 = 0 89 | elif 'oh2' in vc or 'ph2' in vc: 90 | useh2 = 0 91 | useoph2 = 1 92 | else: 93 | # Weird case: no H2 colliders at all 94 | useoph2 = 0 95 | useh2 = 0 96 | 97 | weights = {'H2': 2*useh2, 98 | 'PH2': 2*useoph2, 99 | 'OH2': 2*useoph2, 100 | 'E': 1/1836., 101 | 'H': 1, 102 | 'He': 4, 103 | 'H+': 1,} 104 | 105 | return np.sum( (self.density[k]*weights[k] for k in self.density) 106 | )*constants.m_p 107 | 108 | 109 | @property 110 | def opr(self): 111 | return self.density['OH2']/self.density['PH2'] 112 | 113 | @property 114 | def oprh2(self): 115 | return self.opr 116 | 117 | @property 118 | def species(self): 119 | return self._species 120 | 121 | @species.setter 122 | def species(self, species): 123 | if hasattr(self,'_species') and self._species == species: 124 | return 125 | self._species = species 126 | try: 127 | self.molpath = os.path.join(self.datapath,species+'.dat') 128 | except IOError: 129 | log.warn("Did not find data file for species {0} " 130 | "in path {1}. Downloading it.".format(species, 131 | self.datapath)) 132 | utils.get_datafile(species, self.datapath) 133 | self.molpath = os.path.join(self.datapath,species+'.dat') 134 | 135 | self._valid_colliders = utils.get_colliders(self.molpath) 136 | vc = [x.lower() for x in self._valid_colliders] 137 | if 'h2' in vc and ('oh2' in vc or 'ph2' in vc): 138 | log.warn("oH2/pH2 and h2 are both in the datafile: " 139 | "The resulting density/total density are invalid.") 140 | 141 | @property 142 | def molpath(self): 143 | raise NotImplementedError 144 | 145 | @molpath.setter 146 | def molpath(self, molfile): 147 | raise NotImplementedError 148 | 149 | 150 | @property 151 | def datapath(self): 152 | return os.path.dirname(self.molpath) 153 | 154 | @datapath.setter 155 | def datapath(self, radat): 156 | raise NotImplementedError 157 | 158 | @property 159 | def escapeProbGeom(self): 160 | raise NotImplementedError 161 | 162 | @escapeProbGeom.setter 163 | def escapeProbGeom(self, escapeProbGeom): 164 | raise NotImplementedError 165 | 166 | 167 | @property 168 | def column(self): 169 | return self.column_per_bin 170 | 171 | @column.setter 172 | def column(self, value): 173 | self.column_per_bin = value 174 | 175 | 176 | @property 177 | def column_per_kms_perpc(self): 178 | return self.column_per_bin / self.deltav 179 | 180 | 181 | @column_per_kms_perpc.setter 182 | def column_per_kms_perpc(self, cddv): 183 | 184 | cddv = u.Quantity(cddv, self._u_gradient) 185 | 186 | self.column_per_bin = cddv * u.Quantity(self.deltav, self._u_kms) * self.length() 187 | 188 | @property 189 | def abundance(self): 190 | return self._abundance 191 | 192 | @abundance.setter 193 | def abundance(self, abund): 194 | self._abundance = abund 195 | if not self._is_locked: 196 | assert self.locked_parameter in ('column', 'abundance', 'density') 197 | if self.locked_parameter == 'abundance': # self is locked, still need to update 198 | if hasattr(self, '_previous_locked_parameter'): 199 | self._lock_param(self._previous_locked_parameter) 200 | else: 201 | self._lock_param('density') # choose arbitrarily 202 | self._is_locked = True 203 | if self.locked_parameter == 'column': 204 | dens = self.column_per_bin / self.length / abund 205 | self.density = dens 206 | elif self.locked_parameter == 'density': 207 | col = self.total_density*self.length*abund 208 | self.column_per_bin = u.Quantity(col, u.cm**-2) 209 | self._lock_param('abundance') 210 | self._is_locked=False 211 | np.testing.assert_almost_equal((self.total_density / (self.column / 212 | self.length)), 213 | 1/self.abundance) 214 | 215 | @property 216 | def deltav(self): 217 | return self._deltav 218 | 219 | 220 | @deltav.setter 221 | def deltav(self, dv): 222 | self._deltav = u.Quantity(dv, self._u_kms) 223 | 224 | @property 225 | def length(self): 226 | """ Hard-coded, assumed length-scale """ 227 | return u.Quantity(1, u.pc) 228 | 229 | @property 230 | def tbg(self): 231 | raise NotImplementedError 232 | 233 | def _validate_colliders(self): 234 | """ 235 | Check whether the density of at least one collider in the associated 236 | LAMDA data file is nonzero 237 | """ 238 | valid_colliders = [x.lower() for x in self.valid_colliders] 239 | 240 | density = self.density 241 | 242 | OK = False 243 | matched_colliders = [] 244 | for collider in valid_colliders: 245 | if unitless(density[self._all_valid_colliders[collider.upper()]]) > 0: 246 | OK = True 247 | matched_colliders.append(collider.lower()) 248 | 249 | if not OK: 250 | raise ValueError("The colliders in the data file {0} ".format(self.molpath) 251 | + "have density 0.") 252 | 253 | bad_colliders = [] 254 | for collider in density: 255 | if (unitless(density[collider]) > 0 256 | and (collider.lower() not in valid_colliders)): 257 | if (collider.lower() in ('oh2','ph2') and 'h2' in 258 | matched_colliders): 259 | # All is OK: we're allowed to have mismatches of this sort 260 | continue 261 | elif (collider.lower() == 'h2' and ('oh2' in matched_colliders 262 | or 'ph2' in 263 | matched_colliders)): 264 | # again, all OK 265 | continue 266 | bad_colliders.append(collider) 267 | OK = False 268 | 269 | if not OK: 270 | raise ValueError("There are colliders with specified densities >0 " 271 | "that do not have corresponding collision rates." 272 | " The bad colliders are {0}".format(bad_colliders)) 273 | 274 | 275 | @property 276 | def source_area(self): 277 | if hasattr(self, '_source_area'): 278 | return self._source_area 279 | 280 | @source_area.setter 281 | def source_area(self, source_area): 282 | self._source_area = source_area 283 | 284 | @property 285 | def source_line_surfbrightness(self): 286 | return self.source_brightness - self.background_brightness 287 | 288 | def line_brightness_temperature(self,beamsize): 289 | """ 290 | Return the line surface brightness in kelvins for a given beam area 291 | (Assumes the frequencies are rest frequencies) 292 | """ 293 | #return (self.line_flux * beamsize) 294 | # because each line has a different frequency, have to loop it 295 | try: 296 | return u.Quantity([x.to(u.K, u.brightness_temperature(beam_area=beamsize, frequency=f)).value 297 | for x,f in zip(self.line_flux_density,self.frequency) 298 | ], 299 | unit=u.K) 300 | except AttributeError as ex: 301 | raise NotImplementedError("line brightness temperature is not implemented " 302 | "without reference to astropy units yet") 303 | 304 | @property 305 | def source_line_brightness_temperature(self): 306 | """ 307 | The surface brightness of the source assuming it is observed with a 308 | beam matched to its size and it has ff=1 309 | 310 | (this is consistent with the online RADEX calculator) 311 | """ 312 | #return (self.line_flux * beamsize) 313 | # because each line has a different frequency, have to loop it 314 | return ((self.source_line_surfbrightness*u.sr). 315 | to(u.K, u.brightness_temperature(beam_area=1*u.sr, 316 | frequency=self.frequency))) 317 | 318 | @property 319 | def T_B(self): 320 | return self.source_line_brightness_temperature 321 | 322 | 323 | @property 324 | def background_brightness(self): 325 | raise NotImplementedError 326 | 327 | @property 328 | def flux_density(self): 329 | """ 330 | Convert the source surface brightness to a flux density by specifying 331 | the emitting area of the source (in steradian-equivalent units) 332 | 333 | This is the non-background-subtracted version 334 | """ 335 | 336 | if not self.source_area: 337 | raise AttributeError("Need to specify a source area in order to compute the flux density") 338 | 339 | return self.source_brightness * self.source_area 340 | 341 | @property 342 | def line_flux_density(self): 343 | """ 344 | Background-subtracted version of flux_density 345 | """ 346 | 347 | if not self.source_area: 348 | raise AttributeError("Need to specify a source area in order to compute the flux density") 349 | 350 | return self.source_line_surfbrightness * self.source_area 351 | 352 | 353 | @property 354 | def source_brightness(self): 355 | """ 356 | RADEX compat? (check) 357 | """ 358 | 359 | raise NotImplementedError 360 | 361 | @property 362 | def source_brightness_beta(self): 363 | 364 | raise NotImplementedError 365 | 366 | @property 367 | def beta(self): 368 | raise NotImplementedError 369 | 370 | def get_table(self): 371 | columns = [ 372 | astropy.table.Column(name='Tex', data=self.tex, unit=u.K), 373 | astropy.table.Column(name='tau', data=self.tau, unit=''), 374 | astropy.table.Column(name='frequency', data=self.frequency, 375 | unit=u.GHz), 376 | astropy.table.Column(name='upperstateenergy', 377 | data=self.upperstateenergy, unit=u.K), 378 | astropy.table.Column(name='upperlevel', 379 | data=self.upperlevelnumber, 380 | unit=''), 381 | astropy.table.Column(name='lowerlevel', 382 | data=self.lowerlevelnumber, 383 | unit=''), 384 | astropy.table.Column(name='upperlevelpop', 385 | data=self.upperlevelpop, 386 | unit=''), 387 | astropy.table.Column(name='lowerlevelpop', 388 | data=self.lowerlevelpop, 389 | unit=''), 390 | astropy.table.Column(name='brightness', 391 | data=self.source_line_surfbrightness), 392 | astropy.table.Column(name='T_B', data=self.T_B), # T_B is pre-masked 393 | ] 394 | if self.source_area: 395 | columns.append(astropy.table.Column(name='flux',data=self.line_flux_density[mask])) 396 | 397 | T = astropy.table.Table(columns) 398 | 399 | return T 400 | 401 | def get_synthspec(self, fmin, fmax, npts=1000, **kwargs): 402 | """ 403 | Generate a synthetic spectrum of the selected molecule over the 404 | specified frequency range. This task is good for quick-looks but has a 405 | lot of overhead for generating models and should not be used for 406 | fitting (unless you have a conveniently small amount of data) 407 | 408 | Parameters 409 | ---------- 410 | fmin : `~astropy.units.Quantity` 411 | fmax : `~astropy.units.Quantity` 412 | Frequency-equivalent quantity 413 | """ 414 | wcs = synthspec.FrequencyArray(fmin, fmax, npts) 415 | S = synthspec.SyntheticSpectrum.from_RADEX(wcs, self, **kwargs) 416 | 417 | return S 418 | -------------------------------------------------------------------------------- /pyradex/fjdu/core.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import numpy as np 3 | import os 4 | import warnings 5 | from collections import defaultdict 6 | from astropy import constants 7 | from astropy import log 8 | from .. import base_class 9 | from ..utils import ImmutableDict,unitless,lower_keys 10 | from .. import utils 11 | 12 | import astropy.units as u 13 | _quantity = u.Quantity 14 | 15 | class Fjdu(base_class.RadiativeTransferApproximator): 16 | def __init__(self, datapath=None, species='co', 17 | density=None, 18 | collider_densities=None, 19 | temperature=None, 20 | tbg=2.73, 21 | column=None, 22 | abundance=None, 23 | escapeProbGeom='lvg', 24 | **kwargs): 25 | 26 | if os.getenv('RADEX_DATAPATH') and datapath is None: 27 | datapath = os.getenv('RADEX_DATAPATH') 28 | 29 | self.datapath = os.path.dirname(datapath) 30 | self.species = species 31 | 32 | self._is_locked = False 33 | self._locked_parameter = None 34 | self.set_default_params() 35 | self.set_params(temperature=temperature, density=density, 36 | collider_densities=collider_densities, 37 | column=column, geotype=escapeProbGeom, **kwargs) 38 | self.tbg = tbg 39 | from pyradex.fjdu import wrapper_my_radex 40 | myradex_wrapper = wrapper_my_radex.myradex_wrapper 41 | self._myradex = myradex_wrapper 42 | self._is_locked = False 43 | self._locked_parameter = None 44 | 45 | if abundance is not None: 46 | if density is not None or collider_densities is not None: 47 | self._locked_parameter = 'density' 48 | elif column is not None: 49 | self._locked_parameter = 'column' 50 | else: 51 | raise ValueError("At least two of column, density, and " 52 | "abundance must be specified") 53 | self.abundance = abundance 54 | 55 | def __call__(self, return_table=True, **kwargs): 56 | 57 | niter = self.run_radex(**kwargs) 58 | 59 | if return_table: 60 | return self.get_table() 61 | else: 62 | return niter 63 | 64 | def load_datafile(self, filename=None, verbose=False): 65 | filename = filename or self.molpath 66 | self.datapath = (os.path.dirname(filename) or self.datapath)+"/" 67 | self.fname = os.path.basename(filename) 68 | 69 | nlevels, nitems, ntrans = self._myradex.config_basic(self.datapath, 70 | self.fname, 71 | unitless(self.tbg), 72 | verbose) 73 | self.set_params(**{'n_levels': nlevels, 74 | 'n_item': nitems, 75 | 'n_transitions': ntrans}) 76 | 77 | def run_radex(self, **kwargs): 78 | 79 | # drop kwargs kept for compatibility with Radex 80 | ignore_kwargs = ['reuse_last', 'reload_molfile'] 81 | for ik in ignore_kwargs: 82 | if ik in kwargs: 83 | kwargs.pop(ik) 84 | 85 | self.set_params(**kwargs) 86 | self.load_datafile() 87 | energies, f_occupations, data_transitions, cooling_rate = \ 88 | self._myradex.run_one_params(**self.params) 89 | self._energies = u.Quantity(energies, u.K) # excitation temperature 90 | self._data_dict = cast_into_dic(self._myradex.column_names.tostring().decode(), 91 | data_transitions) 92 | self._level_population = f_occupations 93 | 94 | 95 | _default_params = (('tkin', 0.0), 96 | ('dv_CGS', 1e5), 97 | ('dens_X_CGS', 0.0), 98 | ('Ncol_X_CGS', 0.0), 99 | ('H2_density_CGS', 0.0), 100 | ('HI_density_CGS', 0.0), 101 | ('oH2_density_CGS', 0.0), 102 | ('pH2_density_CGS', 0.0), 103 | ('HII_density_CGS', 0.0), 104 | ('Electron_density_CGS', 0.0), 105 | ('n_levels', 0), 106 | ('n_item', 0), 107 | ('n_transitions', 0), 108 | ('geotype', 'lvg'), 109 | ) 110 | 111 | _keyword_map = {#'temperature': 'tkin', 112 | 'deltav': 'dv_cgs', 113 | #'column': 'ncol_x_cgs', 114 | } 115 | 116 | _density_keyword_map = {'h2': 'h2_density_cgs', 117 | 'h': 'hi_density_cgs', 118 | 'oh2': 'oh2_density_cgs', 119 | 'ph2': 'ph2_density_cgs', 120 | 'hii': 'hii_density_cgs', 121 | 'e': 'electron_density_cgs', 122 | } 123 | 124 | def set_default_params(self): 125 | self._params = lower_keys(dict(self._default_params)) 126 | 127 | def set_params(self, **kwargs): 128 | default = lower_keys(dict(self._default_params)) 129 | for k in kwargs: 130 | if kwargs[k] is None: 131 | continue 132 | if k == 'deltav': 133 | # deltav requires unit conversion 134 | self.deltav = kwargs[k] 135 | elif k.lower() in self._keyword_map: 136 | self._params[self._keyword_map[k]] = kwargs[k] 137 | elif k.lower() in ('density','collider_densities'): 138 | self.density = kwargs[k] 139 | elif k.lower() == 'column': 140 | self.column = kwargs[k] 141 | elif k.lower() == 'temperature': 142 | # temperature _cannot_ be set until density is 143 | if not hasattr(self, '_use_thermal_opr'): 144 | try: 145 | self.density = kwargs['density'] 146 | except KeyError: 147 | self.density = kwargs['collider_densities'] 148 | self.temperature = kwargs[k] 149 | elif k == 'tbg': 150 | self.tbg = kwargs[k] 151 | elif k == 'species': 152 | self.species = kwargs[k] 153 | elif k.lower() not in default: 154 | raise ValueError("{0} is not a valid key.".format(k)) 155 | else: 156 | self._params[k] = kwargs[k] 157 | 158 | 159 | @property 160 | def params(self): 161 | return lower_keys(self._params) 162 | 163 | @params.setter 164 | def params(self, value): 165 | if not isinstance(value, dict): 166 | raise TypeError('Parameters must be a dictionary.') 167 | self.set_params(**value) 168 | 169 | @property 170 | def density(self): 171 | 172 | dd = {'H2': u.Quantity(self.params['h2_density_cgs'], self._u_cc), 173 | 'OH2': u.Quantity(self.params['oh2_density_cgs'], self._u_cc), 174 | 'PH2': u.Quantity(self.params['ph2_density_cgs'], self._u_cc), 175 | 'E': u.Quantity(self.params['electron_density_cgs'], self._u_cc), 176 | 'H+': u.Quantity(self.params['hii_density_cgs'], self._u_cc), 177 | 'H': u.Quantity(self.params['hi_density_cgs'], self._u_cc), 178 | 'He': u.Quantity(0, self._u_cc),} 179 | return ImmutableDict(dd) 180 | 181 | @density.setter 182 | def density(self, collider_density): 183 | 184 | self._use_thermal_opr = False 185 | 186 | if isinstance(collider_density, (float,int,_quantity,np.ndarray)): 187 | log.warn("Assuming the density is n(H_2).") 188 | collider_density = {'H2': collider_density} 189 | 190 | collider_densities = defaultdict(lambda: 0) 191 | for k in collider_density: 192 | collider_densities[k.upper()] = unitless(u.Quantity(collider_density[k], 193 | self._u_cc)) 194 | if k.upper() not in self._all_valid_colliders: 195 | raise ValueError('Collider %s is not one of the valid colliders: %s' % 196 | (k,self._all_valid_colliders)) 197 | 198 | if (('OH2' in collider_densities and collider_densities['OH2'] !=0) or 199 | ('PH2' in collider_densities and collider_densities['PH2'] !=0)): 200 | if not 'PH2' in collider_densities or not 'OH2' in collider_densities: 201 | raise ValueError("If o-H2 density is specified, p-H2 must also be.") 202 | # dictionary of collider densities 203 | for k in collider_densities: 204 | if k.lower() in self._density_keyword_map: 205 | key = self._density_keyword_map[k.lower()] 206 | self._params[key.lower()] = collider_densities[k] 207 | elif k.lower() in self._density_keyword_map.values(): 208 | self._params[k.lower()] = collider_densities[k] 209 | else: 210 | raise KeyError("Collider {0} not recognized.".format(k)) 211 | self._params['dens_x_cgs'] = self.total_density.value 212 | self._use_thermal_opr = False 213 | elif 'H2' in collider_densities and 'H2' in self._valid_colliders: 214 | # H2 is a collider in the file: use it. 215 | self._params['dens_x_cgs'] = collider_density['H2'] 216 | for k in self._density_keyword_map.values(): 217 | self._params[k] = 0.0 218 | self._params['h2_density_cgs'] = collider_density['H2'] 219 | elif 'H2' in collider_densities: 220 | # Only oH2 and pH2 are in the file. Must assume. 221 | warnings.warn("Using a default ortho-to-para ratio (which " 222 | "will only affect species for which independent " 223 | "ortho & para collision rates are given)") 224 | self._use_thermal_opr = True 225 | #self.radex.cphys.density[0] = collider_densities['H2'] 226 | 227 | T = unitless(self.temperature) 228 | if T > 0: 229 | # From Faure, private communication 230 | opr = min(3.0,9.0*np.exp(-170.6/T)) 231 | else: 232 | opr = 3.0 233 | fortho = opr/(1+opr) 234 | log.debug("Set OPR to {0} and fortho to {1}".format(opr,fortho)) 235 | self._params['oh2_density_cgs'] = collider_density['H2']*(fortho) 236 | self._params['ph2_density_cgs'] = collider_density['H2']*(1-fortho) 237 | self._params['dens_x_cgs'] = self.total_density.value 238 | 239 | 240 | @property 241 | def temperature(self): 242 | return u.Quantity(self.params['tkin'], u.K) 243 | 244 | @temperature.setter 245 | def temperature(self, tkin): 246 | if hasattr(tkin,'to'): 247 | tkin = unitless(u.Quantity(tkin, u.K)) 248 | if tkin <= 0 or tkin > 1e4: 249 | raise ValueError('Must have kinetic temperature > 0 and < 10^4 K') 250 | self.set_params(tkin=tkin) 251 | 252 | if self._use_thermal_opr: 253 | # Reset the density to a thermal value 254 | lp = self._locked_parameter 255 | self.density = (unitless(self.density['H2']) or 256 | unitless(self.density['OH2']+self.density['PH2'])) 257 | self._locked_parameter = lp 258 | 259 | @property 260 | def column_per_bin(self): 261 | return u.Quantity(self.params['ncol_x_cgs'], self._u_sc) 262 | 263 | @column_per_bin.setter 264 | def column_per_bin(self, col): 265 | if hasattr(col,'to'): 266 | col = unitless(u.Quantity(col, self._u_sc)) 267 | if col < 1e5 or col > 1e25: 268 | raise ValueError("Extremely low or extremely high column.") 269 | self.set_params(ncol_x_cgs=col) 270 | 271 | col = u.Quantity(col, self._u_sc) 272 | if not self._is_locked: 273 | self._is_locked = True 274 | if self.locked_parameter == 'density': 275 | ab = (col/(self.total_density * self.length)) 276 | if hasattr(ab, 'decompose'): 277 | self.abundance = ab.decompose().value 278 | else: 279 | self.abundance = ab / (self._u_cc*u.pc).to(self._u_sc) 280 | elif self.locked_parameter == 'abundance': 281 | self.density = col / self.length / self.abundance 282 | self._lock_param('column') 283 | self._is_locked = False 284 | 285 | @property 286 | def abundance(self): 287 | return self._abundance 288 | 289 | @abundance.setter 290 | def abundance(self, abund): 291 | self._abundance = abund 292 | if not self._is_locked: 293 | self._is_locked = True 294 | if self.locked_parameter == 'column': 295 | dens = self.column_per_bin / self.length / abund 296 | self.density = dens 297 | elif self.locked_parameter == 'density': 298 | col = self.total_density*self.length*abund 299 | self.column_per_bin = u.Quantity(col, u.cm**-2) 300 | self._lock_param('abundance') 301 | self._is_locked=False 302 | 303 | @property 304 | def tbg(self): 305 | return u.Quantity(self._tbg, u.K) 306 | 307 | @tbg.setter 308 | def tbg(self, tbg): 309 | if hasattr(tbg, 'value'): 310 | self._tbg = unitless(u.Quantity(tbg, u.K)) 311 | else: 312 | self._tbg = tbg 313 | 314 | @property 315 | def deltav(self): 316 | return u.Quantity(self.params['dv_cgs']*self._kms_to_cms, self._u_kms) 317 | 318 | _kms_to_cms = 1e-5 319 | _u_cms = u.cm/u.s 320 | 321 | @deltav.setter 322 | def deltav(self, dv): 323 | if hasattr(dv, 'unit'): 324 | self._params['dv_cgs'] = unitless(dv.to(self._u_cms)) 325 | else: 326 | self._params['dv_cgs'] = unitless(u.Quantity(dv/self._kms_to_cms, 327 | self._u_cms)) 328 | 329 | @property 330 | def molpath(self): 331 | if hasattr(self,'_molpath'): 332 | return self._molpath 333 | 334 | @molpath.setter 335 | def molpath(self, molfile): 336 | if "~" in molfile: 337 | molfile = os.path.expanduser(molfile) 338 | utils.verify_collisionratefile(molfile) 339 | self._molpath = molfile 340 | 341 | @property 342 | def datapath(self): 343 | return self._datapath 344 | 345 | @datapath.setter 346 | def datapath(self, datapath): 347 | self._datapath = datapath 348 | 349 | @property 350 | def escapeprobProbGeom(self): 351 | return self._params['geotype'] 352 | 353 | @escapeprobProbGeom.setter 354 | def escapeprobProbGeom(self, value): 355 | if value in ('lvg','spherical','slab'): 356 | self._params['geotype'] = value 357 | else: 358 | raise ValueError("Geometry must be spherical, slab, or lvg") 359 | 360 | _um_to_ghz = u.um.to(u.GHz, equivalencies=u.spectral()) 361 | 362 | @property 363 | def frequency(self): 364 | return u.Quantity(self._um_to_ghz/self._data_dict['lam'], unit=u.GHz) 365 | 366 | @property 367 | def level_population(self): 368 | return self._level_population 369 | 370 | @property 371 | def tex(self): 372 | return u.Quantity(self._data_dict['Tex'], u.K) 373 | 374 | Tex = tex 375 | 376 | @property 377 | def tau(self): 378 | return self._data_dict['tau'] 379 | 380 | @property 381 | def upperstateenergy(self): 382 | return u.Quantity(self._data_dict['Eup'], u.K) 383 | 384 | @property 385 | def upperlevelnumber(self): 386 | return self._data_dict['iup'] 387 | 388 | @property 389 | def lowerlevelnumber(self): 390 | return self._data_dict['ilow'] 391 | 392 | @property 393 | def upperlevelpop(self): 394 | return self._data_dict['fup'] 395 | 396 | @property 397 | def lowerlevelpop(self): 398 | return self._data_dict['flow'] 399 | 400 | @property 401 | def source_line_brightness_temperature(self): 402 | return u.Quantity(self._data_dict['Tr'], u.K) 403 | 404 | #@property 405 | #def source_line_surfbrightness(self): 406 | # return u.Quantity(self._data_dict['flux'], self._u_brightness) 407 | 408 | @property 409 | def source_brightness(self): 410 | return u.Quantity(self._data_dict['flux_dens'], self._u_brightness) 411 | 412 | @property 413 | def background_brightness(self): 414 | return u.Quantity(self._data_dict['Jback'], self._u_brightness) 415 | # return self.tbg.to(self._u_brightness) 416 | 417 | @property 418 | def beta(self): 419 | return self._data_dict['beta'] 420 | 421 | @property 422 | def statistical_weight(self): 423 | return self._data_dict['gup'] 424 | 425 | 426 | def cast_into_dic(col_names, arr): 427 | '''col_names is column_info, and arr is data_transitions''' 428 | names = col_names.split() 429 | return {names[i]: arr[i,:] for i in range(len(names))} 430 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python RADEX interface 2 | ====================== 3 | 4 | A wrapper for RADEX (www.sron.rug.nl/~vdtak/radex/) in python. 5 | 6 | As of v0.2, created October 26, 2013, this package includes both a python 7 | wrapper of the command-line program and a direct wrapper of the fortran code 8 | created with f2py. 9 | 10 | Installation procedure for the f2py-wrapped version 11 | --------------------------------------------------- 12 | 13 | You need to have `gfortran` and `f2py` on your path. If you've successfully 14 | built numpy from source, you should have both. 15 | 16 | You need to clone this repository first with `--recursive` enabled so that `myRadex `_ is downloaded: 17 | 18 | .. code-block:: bash 19 | 20 | git clone --recursive https://github.com/keflavich/pyradex.git 21 | 22 | Then `cd` to the source directory and run: 23 | 24 | .. code-block:: bash 25 | 26 | $ python setup.py install_radex install_myradex install 27 | 28 | This will call a procedure `install_radex` that downloads the latest version of 29 | RADEX from the radex homepage, patches the source, and builds a file `radex.so`, 30 | which is a python shared object that can be imported. 31 | 32 | See the install_ page for more details. 33 | 34 | If you want pyradex to look in a specific directory for the molecular data 35 | files, you can specify an environmental variable ``RADEX_DATAPATH`` prior to 36 | starting python. It can also be specified interactively with the ``datapath`` 37 | keyword. If you do not specify one of these two variables, the code 38 | will not work and may return strange errors. 39 | 40 | 41 | Using the f2py-wrapped version 42 | ------------------------------ 43 | 44 | The direct wrapper of the fortran code uses a class `Radex` as its underlying 45 | structure. This class is useful for direct manipulation of RADEX inputs and 46 | direct access to its outputs. 47 | 48 | Example (assuming ``RADEX_DATAPATH`` has been specified as an environmental variable): 49 | 50 | .. code-block:: python 51 | 52 | import pyradex 53 | import numpy as np 54 | R = pyradex.Radex(collider_densities={'oH2':900,'pH2':100}, column=1e16, species='co', temperature=20) 55 | Tlvg = R(escapeProbGeom='lvg') 56 | Tslab = R(escapeProbGeom='slab') 57 | Tsphere = R(escapeProbGeom='sphere') 58 | Tlvg[:3].pprint() 59 | Tslab[:3].pprint() 60 | Tsphere[:3].pprint() 61 | 62 | Result:: 63 | 64 | Tex tau frequency upperstateenergy upperlevel lowerlevel upperlevelpop lowerlevelpop flux 65 | ------------- -------------- ----------- ---------------- ---------- ---------- ---------------- --------------- ----------------- 66 | 15.2747101724 0.937692338925 115.2712018 5.53 2 1 0.273140336953 0.453621905471 2.93964536078e-14 67 | 10.8673211326 2.74275175782 230.538 16.6 3 2 0.0518618367484 0.273140336953 9.26125039465e-14 68 | 8.30670325364 2.01021823976 345.7959899 33.19 4 3 0.00379591658449 0.0518618367484 8.16324298598e-14 69 | Tex tau frequency upperstateenergy upperlevel lowerlevel upperlevelpop lowerlevelpop flux 70 | ------------- -------------- ----------- ---------------- ---------- ---------- ---------------- -------------- ----------------- 71 | 17.8076937528 0.681341951256 115.2712018 5.53 2 1 0.312979158313 0.394862780876 2.89304678735e-14 72 | 14.8865118666 1.96024230849 230.538 16.6 3 2 0.102821702575 0.312979158313 1.38012283784e-13 73 | 11.448407058 2.03949857132 345.7959899 33.19 4 3 0.00920322307626 0.102821702575 1.6139902821e-13 74 | Tex tau frequency upperstateenergy upperlevel lowerlevel upperlevelpop lowerlevelpop flux 75 | ------------- ------------- ----------- ---------------- ---------- ---------- ---------------- -------------- ----------------- 76 | 14.38256087 1.06765591906 115.2712018 5.53 2 1 0.243400727834 0.480559204909 2.93394133644e-14 77 | 9.28920337666 3.1666639484 230.538 16.6 3 2 0.037299201561 0.243400727834 7.24810556601e-14 78 | 7.50189023571 1.84556901411 345.7959899 33.19 4 3 0.00307839203073 0.037299201561 6.19215196139e-14 79 | 80 | 81 | Note that because of how RADEX was written, i.e. with common blocks, the values 82 | stored in each of these objects is identical! You cannot have two independent 83 | copies of the RADEX class *ever*. 84 | 85 | Examples 86 | -------- 87 | There is a rich examples gallery. We have a few notebooks: 88 | 89 | http://nbviewer.ipython.org/github/keflavich/pyradex/blob/master/examples/pH2CO_interactive.ipynb 90 | http://nbviewer.ipython.org/github/keflavich/pyradex/blob/master/examples/FittingTheGrid.ipynb 91 | http://nbviewer.ipython.org/github/keflavich/pyradex/blob/master/examples/Interactive.ipynb 92 | http://nbviewer.ipython.org/github/keflavich/pyradex/blob/master/examples/oH2CO-interactive.ipynb 93 | http://nbviewer.ipython.org/github/keflavich/pyradex/blob/master/examples/pH2CO_interactive.ipynb 94 | http://nbviewer.ipython.org/github/keflavich/pyradex/blob/master/examples/ph2co_interactive_mm.ipynb 95 | 96 | and a series of more involved examples: 97 | 98 | * examples/ch3cn_110_synthspec.py 99 | * examples/h2co_grids.py 100 | * examples/h2cs_thermometer.py 101 | * examples/interactive_setup_mm.py 102 | * examples/oh2co_density_grid.py 103 | * examples/oh2co_distributions.py 104 | * examples/oh2co_grids_2.py 105 | * examples/ph2co_grid_computation.py 106 | * examples/ph2co_grid_computation_mm.py 107 | * examples/ph2co_grids.py 108 | * examples/ph2co_grids_2.py 109 | * examples/ph2co_required_sn.py 110 | * examples/simple_co.py 111 | * examples/simple_co_column.py 112 | * examples/synthspec_ch3cn.py 113 | * examples/timing.py 114 | 115 | Most of these were written to make sensitivity estimates for observing proposals. 116 | 117 | Recommended installation procedure for the command-line version 118 | --------------------------------------------------------------- 119 | 120 | 1. `make` radex as normal, but create two executables: `radex_sphere`, `radex_lvg`, and `radex_slab` by 121 | building with one of these three lines commented out each time:: 122 | 123 | c parameter (method = 1) ! uniform sphere 124 | parameter (method = 2) ! expanding sphere (LVG) 125 | c parameter (method = 3) ! plane parallel slab (shock) 126 | 127 | 2. Copy these to your system path 128 | 3. `python setup.py install` to install pyradex 129 | 130 | 131 | Simple example 132 | -------------- 133 | Using some trivial defaults:: 134 | 135 | In [1]: import pyradex 136 | 137 | In [2]: T = pyradex.radex(collider_densities={'H2':1000}) 138 | WARNING: Assumed thermal o/p ratio since only H2 was given but collider file has o- and p- H2 [pyradex.core] 139 | 140 | In [3]: T.pprint(show_units=True) 141 | J_up J_low E_UP FREQ WAVE T_EX TAU T_R POP_UP POP_LOW FLUX_Kkms FLUX_Inu 142 | K GHz um K K K km / s erg / (cm2 s) 143 | ---- ----- ---- -------- --------- ----- --------- ------- ------ ------- --------- ------------- 144 | 1 0 5.5 115.2712 2600.7576 5.044 0.0004447 0.00086 0.4709 0.47 0.0009155 1.806e-11 145 | 146 | In [4]: T.meta 147 | Out[4]: 148 | {'Column density [cm-2]': '1.000E+12', 149 | 'Density of H2 [cm-3]': '1.000E+03', 150 | 'Density of oH2 [cm-3]': '3.509E-04', 151 | 'Density of pH2 [cm-3]': '1.000E+03', 152 | 'Geometry': 'Uniform sphere', 153 | 'Line width [km/s]': '1.000', 154 | 'Molecular data file': '/Users/adam/repos/Radex/data/co.dat', 155 | 'Radex version': '20nov08', 156 | 'T(background) [K]': '2.730', 157 | 'T(kin) [K]': '10.000'} 158 | 159 | 160 | 161 | 162 | Timing information 163 | ------------------ 164 | i.e., how fast is it?:: 165 | 166 | %timeit T = pyradex.pyradex(collider_densities={'H2':1000}) 167 | 10 loops, best of 3: 31.8 ms per loop 168 | 169 | for n in 10**np.arange(6): 170 | %timeit T = pyradex.pyradex(collider_densities={'H2':n}) 171 | 172 | 10 loops, best of 3: 32.1 ms per loop 173 | 10 loops, best of 3: 32.5 ms per loop 174 | 10 loops, best of 3: 32 ms per loop 175 | 10 loops, best of 3: 32.1 ms per loop 176 | 10 loops, best of 3: 32.4 ms per loop 177 | 10 loops, best of 3: 31.9 ms per loop 178 | 179 | for n in 10**np.arange(12,18): 180 | %timeit T = pyradex.pyradex(collider_densities={'H2':1000}, column=n) 181 | 182 | 10 loops, best of 3: 31.8 ms per loop 183 | 10 loops, best of 3: 32.2 ms per loop 184 | 10 loops, best of 3: 32.5 ms per loop 185 | 10 loops, best of 3: 32.2 ms per loop 186 | 10 loops, best of 3: 32.7 ms per loop 187 | 10 loops, best of 3: 33.1 ms per loop 188 | 189 | 190 | If you redo these tests comparing the fortran wrapper to the "naive" version, 191 | the difference can be enormous. The following tests can be seen in `timing.py 192 | `__: 193 | 194 | :: 195 | 196 | 197 | Python external call: 0.0323288917542 198 | Fortran-wrapped: 0.0183672904968 199 | Fortran-wrapped, no reload: 0.000818204879761 200 | Fortran-wrapped, no reload, reuse: 0.000756096839905 201 | Fortran (call method): 0.0270668029785 202 | py/fortran: 1.76013395986 203 | py/fortran, __call__ method: 1.1944111678 204 | py/fortran, no reload: 39.5119762224 205 | py/fortran, no reload, reuse: 42.7576072904 206 | Python external call: 0.0332223176956 207 | Fortran-wrapped: 0.0169018030167 208 | Fortran-wrapped, no reload: 0.000811815261841 209 | Fortran-wrapped, no reload, reuse: 0.000753211975098 210 | Fortran (call method): 0.0275466918945 211 | py/fortran: 1.96560790957 212 | py/fortran, __call__ method: 1.20603656594 213 | py/fortran, no reload: 40.9234948605 214 | py/fortran, no reload, reuse: 44.1075272221 215 | Python external call: 0.0312483787537 216 | Fortran-wrapped: 0.0216565847397 217 | Fortran-wrapped, no reload: 0.00535380840302 218 | Fortran-wrapped, no reload, reuse: 0.000751805305481 219 | Fortran (call method): 0.031253194809 220 | py/fortran: 1.44290427735 221 | py/fortran, __call__ method: 0.999845901985 222 | py/fortran, no reload: 5.83666362361 223 | py/fortran, no reload, reuse: 41.5644562839 224 | Python external call: 0.0316061973572 225 | Fortran-wrapped: 0.0228497028351 226 | Fortran-wrapped, no reload: 0.00549430847168 227 | Fortran-wrapped, no reload, reuse: 0.000753903388977 228 | Fortran (call method): 0.031331205368 229 | py/fortran: 1.38322137427 230 | py/fortran, __call__ method: 1.00877693615 231 | py/fortran, no reload: 5.75253419427 232 | py/fortran, no reload, reuse: 41.9234053319 233 | Python external call: 0.0318208932877 234 | Fortran-wrapped: 0.0216773033142 235 | Fortran-wrapped, no reload: 0.00544350147247 236 | Fortran-wrapped, no reload, reuse: 0.000751280784607 237 | Fortran (call method): 0.0315539121628 238 | py/fortran: 1.46793597093 239 | py/fortran, __call__ method: 1.0084611101 240 | py/fortran, no reload: 5.84566633234 241 | py/fortran, no reload, reuse: 42.3555266415 242 | Python external call: 0.0322543859482 243 | Fortran-wrapped: 0.0225975990295 244 | Fortran-wrapped, no reload: 0.00569999217987 245 | Fortran-wrapped, no reload, reuse: 0.00075900554657 246 | Fortran (call method): 0.0314954996109 247 | py/fortran: 1.42733685583 248 | py/fortran, __call__ method: 1.02409507221 249 | py/fortran, no reload: 5.65867196486 250 | py/fortran, no reload, reuse: 42.4955866185 251 | [ 0.006951 0.006911 0.006956] 252 | [ 0.006951 0.006911 0.006956] 253 | [ 0.006951 0.006911 0.006956] 254 | pyradex.pyradex timing for a 3^4 grid: [2.6063590049743652, 2.598068952560425, 2.592205047607422] 255 | [ 0.00694859 0.00690934 0.00695345] 256 | [ 0.00694859 0.00690934 0.00695345] 257 | [ 0.00694859 0.00690934 0.00695345] 258 | pyradex.Radex() timing for a 3^4 grid: [3.8620870113372803, 3.838628053665161, 3.805685043334961] 259 | [ 0.00694859 0.00690934 0.00695345] 260 | [ 0.00694859 0.00690934 0.00695345] 261 | [ 0.00694859 0.00690934 0.00695345] 262 | pyradex.Radex() class-based timing for a 3^4 grid: [3.1014058589935303, 3.2805678844451904, 3.160888195037842] 263 | [ 0.00694859 0.00690934 0.00695345] 264 | [ 0.00694859 0.00690934 0.00695345] 265 | [ 0.00694859 0.00690934 0.00695345] 266 | pyradex.Radex() class-based timing for a 3^4 grid, using optimal parameter-setting order: [0.9963750839233398, 1.0024840831756592, 0.9699358940124512] 267 | 268 | 269 | Making Grids 270 | ------------ 271 | Is more efficient with scripts, but you can still do it... :: 272 | 273 | R = pyradex.Radex(species='co', collider_densities={'H2':1000}, column=1e15) 274 | for n in 10**np.arange(12,18): 275 | T = R(collider_densities={'H2':1000}, column=n) 276 | T[:1].pprint() 277 | 278 | Tex tau frequency upperstateenergy upperlevel lowerlevel upperlevelpop lowerlevelpop brightness T_B 279 | K GHz K erg / (cm2 Hz s sr) K 280 | ------------- ----------------- ----------- ---------------- ---------- ---------- -------------- -------------- ------------------- ---------------- 281 | 11.0274813968 0.000166783361591 115.2712018 5.53 1 0 0.540537331305 0.297561763825 5.20877418593e-18 0.00127591598469 282 | Tex tau frequency upperstateenergy upperlevel lowerlevel upperlevelpop lowerlevelpop brightness T_B 283 | K GHz K erg / (cm2 Hz s sr) K 284 | ------------- ---------------- ----------- ---------------- ---------- ---------- -------------- -------------- ------------------- --------------- 285 | 11.0274813968 0.00166783361591 115.2712018 5.53 1 0 0.540537331305 0.297561763825 5.2048669339e-17 0.0127495888324 286 | Tex tau frequency upperstateenergy upperlevel lowerlevel upperlevelpop lowerlevelpop brightness T_B 287 | K GHz K erg / (cm2 Hz s sr) K 288 | ------------- --------------- ----------- ---------------- ---------- ---------- -------------- -------------- ------------------- -------------- 289 | 10.9980972475 0.0166790919823 115.2712018 5.53 1 0 0.538730147174 0.296964688622 5.14681095066e-16 0.126073777202 290 | Tex tau frequency upperstateenergy upperlevel lowerlevel upperlevelpop lowerlevelpop brightness T_B 291 | K GHz K erg / (cm2 Hz s sr) K 292 | ------------- -------------- ----------- ---------------- ---------- ---------- -------------- -------------- ------------------- ------------- 293 | 11.7797140751 0.150601068675 115.2712018 5.53 1 0 0.530489509066 0.282823341198 4.78772386104e-15 1.17277754545 294 | Tex tau frequency upperstateenergy upperlevel lowerlevel upperlevelpop lowerlevelpop brightness T_B 295 | K GHz K erg / (cm2 Hz s sr) K 296 | ------------- -------------- ----------- ---------------- ---------- ---------- -------------- -------------- ------------------- ------------- 297 | 15.0692631019 0.955344506002 115.2712018 5.53 1 0 0.454752879863 0.218821739485 2.92170292028e-14 7.15686133711 298 | Tex tau frequency upperstateenergy upperlevel lowerlevel upperlevelpop lowerlevelpop brightness T_B 299 | K GHz K erg / (cm2 Hz s sr) K 300 | ------------- ------------- ----------- ---------------- ---------- ---------- -------------- -------------- ------------------- ------------- 301 | 22.6356250741 4.17742617995 115.2712018 5.53 1 0 0.318586709967 0.135596426565 7.69430015071e-14 18.8475833332 302 | 303 | If you want to create a grid with the directly wrapped version, do loops with 304 | constant temperature: every time you load a new temperature, RADEX must read in 305 | the molecular data file and interpolate across the collision rate values, which 306 | may be a substantial overhead. 307 | 308 | If you want to build a grid, *do not* make an astropy table each time! That 309 | appears to dominate the overhead at each iteration. 310 | 311 | A note on self-consistency in LVG calculations 312 | ---------------------------------------------- 313 | 314 | LVG computations have weird units. The opacity of a line only depends on the 315 | velocity-coherent column along the line of sight, i.e. the column per km/s. 316 | 317 | The key assumption in the LVG Sobolev approximation is that each "cell" can be 318 | treated independently such that there are no nonlocal radiative effects. 319 | 320 | This independence implies that there is a separation between the local volume 321 | density and the total line-of-sight column density. 322 | 323 | However, the quantities reported by typical codes - RADEX, DESPOTIC - are 324 | integrated line-of-sight values. The column density, abundance, and local 325 | volume density are not independent, then. 326 | 327 | In order to have a self-consistent cloud (or line of sight), you must assume 328 | some length scale. Usually, one specifies a velocity gradient per length scale 329 | rather than an absolute length scale, but the length scale is important. 330 | 331 | If a total column density of hydrogen `N(H)` is specified along with a density 332 | `n(H)`, the length scale is trivial: `N(H)/n(H) = L`. If you increase the 333 | density, this length scale decreases - so far all is fine. 334 | 335 | Within RADEX, the standard free variable is the column of the molecule of 336 | interest. 337 | If you change the column of the molecule, which is possible to do explicitly, 338 | and hold everything else fixed in RADEX (`n(H)`, `dV`), the change can be 339 | interpreted as a change in the size scale or the column. 340 | 341 | One could consider the alternative possibility of treating the length scale as 342 | a free parameter, but this approach contains a danger of changing the 343 | interpretation of the processes involved: if the length scale is decreased for 344 | a fixed delta-V, the velocity gradient `dv/dl` must be larger. This 345 | interpretation should be avoided as it bears the risk of breaking the LVG 346 | assumption. The velocity gradient is also often an imposed constraint via the 347 | observed linewidth, while the length scale is only weakly constrained in most 348 | situations. 349 | 350 | In DESPOTIC, the free variables are the total column density, the density, 351 | the abundance, and the velocity gradient. Length is therefore left as the 352 | dependent variable, consistent with the above. 353 | 354 | The Classes (`Despotic` & `Radex`) are constructed such that length is a 355 | dependent variable and all the others can be changed. Since abundance is not 356 | an explicit input into RADEX, this is done with some property machinery behind 357 | the scenes. In v0.3, the length in Radex has been fixed to 1 pc. 358 | 359 | 360 | .. image:: https://d2weczhvl823v0.cloudfront.net/keflavich/pyradex/trend.png 361 | :alt: Bitdeli badge 362 | :target: https://bitdeli.com/free 363 | 364 | .. _install: INSTALL.rst 365 | 366 | --------------------------------------------------------------------------------