├── tests ├── __init__.py ├── test_propagation.py ├── test_intensities.py ├── test_tot_fluxes.py ├── test_s_fluxes.py └── test_spectra.py ├── docs ├── short_example_plot.png ├── total_underground_flux.png ├── Installing_PROPOSAL_on_a_Mac.md ├── Tutorial_Cluster.md ├── Tutorial_Models.md ├── Tutorial_Labs.md └── Tutorial.md ├── examples ├── cluster │ ├── example_submission.sh │ └── example_cluster.py └── example_underground_flux.ipynb ├── .github └── workflows │ ├── pr_tests.yml │ └── build_publish.yml ├── pyproject.toml ├── LICENSE ├── .gitignore ├── README.md ├── src └── mute │ ├── __init__.py │ ├── propagation.py │ ├── constants.py │ └── surface.py └── CHANGELOG.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/short_example_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjwoodley/mute/HEAD/docs/short_example_plot.png -------------------------------------------------------------------------------- /docs/total_underground_flux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wjwoodley/mute/HEAD/docs/total_underground_flux.png -------------------------------------------------------------------------------- /examples/cluster/example_submission.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #SBATCH --account=ACCOUNT_HERE 4 | #SBATCH --time=48:00:00 5 | #SBATCH --cpus-per-task=1 6 | #SBATCH --mail-user=EMAIL_HERE 7 | #SBATCH --mail-type=ALL 8 | 9 | #SBATCH --job-name=NAME_HERE 10 | #SBATCH --output=DIRECTORY_HERE/%x-%j.out 11 | #SBATCH --error=DIRECTORY_HERE/%x-%j.err 12 | 13 | #SBATCH --array=0-99%50 14 | 15 | module load scipy-stack 16 | 17 | python3 example_cluster.py $SLURM_ARRAY_TASK_ID -------------------------------------------------------------------------------- /tests/test_propagation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | 4 | import numpy as np 5 | 6 | import mute.constants as mtc 7 | import mute.propagation as mtp 8 | 9 | try: 10 | 11 | import proposal as pp 12 | 13 | except ImportError: 14 | 15 | pass 16 | 17 | 18 | @pytest.mark.skipif("proposal" not in sys.modules, reason="Requires proposal.") 19 | def test_propagation(): 20 | 21 | mtc.clear() 22 | 23 | mtc.set_n_muon(3) 24 | mtp._create_propagator(force=True) 25 | pp.RandomGenerator.get().set_seed(500) 26 | 27 | u_energy_calc = mtp._propagation_loop( 28 | mtc.ENERGIES[50], mtc.slant_depths[0], force=True 29 | ) 30 | u_energy_read = [8088234.57870225, 7698468.554854496, 7568635.147518327] 31 | 32 | assert np.allclose(u_energy_calc, u_energy_read) 33 | -------------------------------------------------------------------------------- /.github/workflows/pr_tests.yml: -------------------------------------------------------------------------------- 1 | name: PR Tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python-version: ["3.9", "3.13"] 12 | os: [ubuntu-latest, macos-latest] 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -e .[test] 24 | 25 | - name: Test with pytest 26 | run: | 27 | python -m pip install pytest 28 | pytest -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "setuptools-scm", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "mute" 7 | version = "3.0.0" 8 | description = "Muon Intensity Code" 9 | readme = "README.md" 10 | authors = [{ name = "William Woodley", email = "wwoodley@ualberta.ca" }] 11 | license = { text = "BSD 3-Clause License" } 12 | classifiers = [ 13 | "Programming Language :: Python", 14 | "Programming Language :: Python :: 3", 15 | "Operating System :: OS Independent", 16 | "Topic :: Scientific/Engineering :: Physics", 17 | "Intended Audience :: Science/Research", 18 | "License :: OSI Approved :: BSD License", 19 | ] 20 | dependencies = [ 21 | "mceq>=1.3", 22 | "daemonflux", 23 | "numpy", 24 | "requests", 25 | "scipy", 26 | "tqdm", 27 | ] 28 | 29 | [project.urls] 30 | Changelog = "https://github.com/wjwoodley/mute/blob/main/CHANGELOG.md" 31 | Documentation = "https://github.com/wjwoodley/mute/blob/main/docs/Tutorial.md" 32 | Homepage = "https://github.com/wjwoodley/mute" 33 | Issues = "https://github.com/wjwoodley/mute/issues" -------------------------------------------------------------------------------- /tests/test_intensities.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import mute.constants as mtc 6 | import mute.underground as mtu 7 | 8 | 9 | def test_u_intensities(): 10 | 11 | mtc.clear() 12 | 13 | u_intensities_calc = mtu.calc_u_intensities( 14 | method="sd", model="daemonflux", output=False, force=True 15 | ) 16 | u_intensities_read = [ 17 | 6.94919658e-06, 18 | 1.80906517e-06, 19 | 7.24444624e-07, 20 | 3.32127337e-07, 21 | 1.70705322e-07, 22 | 9.22534290e-08, 23 | 5.07858934e-08, 24 | 2.85312282e-08, 25 | 1.63857159e-08, 26 | 9.51599387e-09, 27 | 5.56572979e-09, 28 | 3.27576784e-09, 29 | 1.89893763e-09, 30 | 1.10476908e-09, 31 | 6.45823164e-10, 32 | 3.79235329e-10, 33 | 2.23574504e-10, 34 | 1.31776529e-10, 35 | 7.82436124e-11, 36 | 4.62467535e-11, 37 | 2.74526405e-11, 38 | 1.62848533e-11, 39 | 9.64117005e-12, 40 | 5.71209783e-12, 41 | 3.39018880e-12, 42 | 2.00295030e-12, 43 | 1.17807861e-12, 44 | 6.95181479e-13, 45 | ] 46 | 47 | assert np.allclose(u_intensities_calc, u_intensities_read) 48 | -------------------------------------------------------------------------------- /docs/Installing_PROPOSAL_on_a_Mac.md: -------------------------------------------------------------------------------- 1 | # Installing PROPOSAL on a Mac 2 | 3 | Tested with Mac OS Version 15.4.1. 4 | 5 | ## Requirements 6 | 7 | The following are required to set up a proper environment to install PROPOSAL: 8 | 9 | * ``python3`` 10 | * ``brew`` 11 | * ``cmake`` 12 | * ``g++`` 13 | * ``xcode`` 14 | 15 | Update Homebrew and install ``gcc`` (to compile PROPOSAL) and ``xcode`` (for command line tools): 16 | 17 | ``` 18 | brew update 19 | brew upgrade 20 | brew install gcc 21 | xcode-select --install 22 | ``` 23 | 24 | Unlock Conan: 25 | 26 | ``` 27 | conan remove --locks 28 | ``` 29 | 30 | ## Set the Python Environment up 31 | 32 | Use Python version 3.6 or higher. For example: 33 | 34 | ``` 35 | brew install pyenv 36 | pyenv install 3.8.10 37 | pyenv global 3.8.10 38 | eval "$(pyenv init -)" 39 | python -V 40 | ``` 41 | 42 | ## Install PROPOSAL 43 | 44 | Continue with the installation of PROPOSAL with ``pip``. MUTE has been tested with PROPOSAL v7.6.2; earlier or later versions are not guaranteed to work. 45 | 46 | ``` 47 | python3 -m pip install proposal==7.6.2 48 | ``` 49 | 50 | Verify that PROPOSAL imports properly in Python: 51 | 52 | ``` 53 | python3 54 | 55 | >>> import proposal 56 | >>> 57 | ``` 58 | 59 | If this works, PROPOSAL has been installed. -------------------------------------------------------------------------------- /examples/cluster/example_cluster.py: -------------------------------------------------------------------------------- 1 | # This is an example of a simple script that can be sent to a computer cluster, such as Compute Canada, to run a high number of muons. It can be submitted to slurm with sbatch example_submission.sh. 2 | # With the job array going from 0 to 99 in example_submission.sh and the number of muons set to 1000 in this file, this will give a total of 1e5 muons. 3 | # This will output 100 files of underground muon energies which can be read into MUTE with the function mtp.calc_survival_probability_tensor() with the parameter n_job set to 100. 4 | # Set the force parameter in mtp.propagate_muons() to True to force the creation of any required directories or files (so the program does not wait for your input until the job times out). 5 | # Make sure proposal is installed on the cluster, as the mtp.propagate_muons() function needs it to run the Monte Carlo simulation for the propagation of the muons. 6 | 7 | # Import packages 8 | 9 | import argparse 10 | 11 | import mute.constants as mtc 12 | import mute.propagation as mtp 13 | 14 | # Parse the job array number from the command line 15 | 16 | parser = argparse.ArgumentParser() 17 | parser.add_argument("job_array_number", type=int) 18 | args = parser.parse_args() 19 | 20 | # Set the constants 21 | 22 | mtc.set_verbose(0) 23 | mtc.set_output(True) 24 | mtc.set_directory("mute/data") 25 | mtc.set_lab("Example") 26 | mtc.set_n_muon(1000) 27 | 28 | # Propagate the muons 29 | 30 | mtp.propagate_muons(seed=args.job_array_number, job_array_number=args.job_array_number, force=True) 31 | -------------------------------------------------------------------------------- /tests/test_tot_fluxes.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import mute.constants as mtc 6 | import mute.surface as mts 7 | import mute.underground as mtu 8 | 9 | 10 | def test_s_tot_flux(): 11 | 12 | mtc.clear() 13 | 14 | s_fluxes = mts.load_s_fluxes_from_file( 15 | model="mceq", interaction_model="sibyll23c", primary_model="hg", output=False 16 | ) 17 | 18 | s_tot_flux_calc = mts.calc_s_tot_flux(s_fluxes=s_fluxes) 19 | s_tot_flux_read = 0.011553938671563626 20 | 21 | assert np.allclose(s_tot_flux_calc, s_tot_flux_read) 22 | 23 | 24 | def test_u_tot_flux_flat(): 25 | 26 | mtc.clear() 27 | 28 | mtc.set_output(False) 29 | mtc.set_overburden("flat") 30 | mtc.set_vertical_depth(3.5) 31 | 32 | u_fluxes = mtu.calc_u_fluxes(full_tensor=True, model="daemonflux", output=False) 33 | 34 | u_tot_flux_calc = mtu.calc_u_tot_flux(u_fluxes=u_fluxes, E_th=100, force=True) 35 | u_tot_flux_read = 1.4480871762241838e-08 36 | 37 | assert np.allclose(u_tot_flux_calc, u_tot_flux_read) 38 | 39 | 40 | def test_u_tot_flux_mountain(): 41 | 42 | mtc.clear() 43 | 44 | mtc.set_output(False) 45 | mtc.set_overburden("mountain") 46 | 47 | mountain_path = os.path.join( 48 | os.path.dirname(__file__), "example_mountain_profile.txt" 49 | ) 50 | 51 | mtc.load_mountain(mountain_path) 52 | 53 | u_tot_flux_calc = mtu.calc_u_tot_flux( 54 | model="mceq", interaction_model="ddm", output=False, force=True 55 | ) 56 | u_tot_flux_read = 1.400753073325132e-06 57 | 58 | assert np.allclose(u_tot_flux_calc, u_tot_flux_read) 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, William Woodley 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /.github/workflows/build_publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' # Trigger on version tags like v1.0.0 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 15 | os: [ubuntu-latest, macos-latest] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install -e .[test] # Assumes test dependencies are in pyproject.toml [project.optional-dependencies.test] 26 | - name: Test with pytest 27 | run: | 28 | python -m pip install pytest 29 | pytest 30 | 31 | build_and_publish: 32 | needs: test 33 | runs-on: ubuntu-latest # Pure Python package, build and publish on Ubuntu 34 | if: startsWith(github.ref, 'refs/tags/v') # Only publish on version tags 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Set up Python 3.9 38 | uses: actions/setup-python@v5 39 | with: 40 | python-version: "3.9" 41 | - name: Install build dependencies 42 | run: | 43 | python -m pip install --upgrade pip 44 | pip install build 45 | - name: Build distributions 46 | run: python -m build # Builds sdist and wheel 47 | - name: Publish to PyPI 48 | uses: pypa/gh-action-pypi-publish@release/v1 49 | with: 50 | user: __token__ 51 | password: ${{ secrets.PYPI_API_TOKEN }} # Store your PyPI token as a secret in GitHub -------------------------------------------------------------------------------- /tests/test_s_fluxes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import sys 4 | 5 | import mute.constants as mtc 6 | import mute.surface as mts 7 | 8 | try: 9 | 10 | import crflux.models as pm 11 | 12 | except ImportError: 13 | 14 | pass 15 | 16 | 17 | def test_s_fluxes(): 18 | 19 | mtc.clear() 20 | 21 | s_fluxes_calc = mts.calc_s_fluxes(model="daemonflux", output=False, test=True) 22 | s_fluxes_read = mts.load_s_fluxes_from_file(model="daemonflux", test=True) 23 | 24 | assert np.allclose(s_fluxes_calc, s_fluxes_read) 25 | 26 | 27 | def test_s_fluxes_mceq(): 28 | 29 | mtc.clear() 30 | 31 | s_fluxes_calc = mts.calc_s_fluxes( 32 | model="mceq", interaction_model="sibyll23c", output=False, test=True 33 | ) 34 | s_fluxes_read = mts.load_s_fluxes_from_file( 35 | model="mceq", interaction_model="sibyll23c", test=True 36 | ) 37 | 38 | assert np.allclose(s_fluxes_calc, s_fluxes_read) 39 | 40 | 41 | @pytest.mark.skipif("crflux.models" not in sys.modules, reason="Requires crflux.") 42 | def test_s_fluxes_primary_model(): 43 | 44 | mtc.clear() 45 | 46 | s_fluxes_calc = mts.calc_s_fluxes( 47 | model="mceq", 48 | primary_model=(pm.GaisserStanevTilav, "3-gen"), 49 | output=False, 50 | test=True, 51 | ) 52 | s_fluxes_read = mts.load_s_fluxes_from_file( 53 | model="mceq", primary_model="gst3", test=True 54 | ) 55 | 56 | assert np.allclose(s_fluxes_calc, s_fluxes_read) 57 | 58 | 59 | def test_s_fluxes_location(): 60 | 61 | mtc.clear() 62 | 63 | s_fluxes_calc = mts.calc_s_fluxes( 64 | model="mceq", 65 | interaction_model="sibyll23c", 66 | atmosphere="msis00", 67 | month="July", 68 | location=(46.472, -81.187), 69 | output=False, 70 | test=True, 71 | ) 72 | s_fluxes_read = mts.load_s_fluxes_from_file( 73 | model="mceq", 74 | interaction_model="sibyll23c", 75 | atmosphere="msis00", 76 | month="July", 77 | location=(46.472, -81.187), 78 | test=True, 79 | ) 80 | 81 | assert np.allclose(s_fluxes_calc, s_fluxes_read) 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | */.ipynb_checkpoints/ 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | src/mute/data 133 | .venv_* 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MUTE 2 | 3 | [![DOI](https://zenodo.org/badge/DOI/10.5282/zenodo.5791812.svg)](https://doi.org/10.5281/zenodo.5791812) 4 | 5 | MUTE (**MU**on in**T**ensity cod**E**) is a computational tool for calculating atmospheric muon fluxes and intensities underground. It makes use of the state-of-the-art codes [daemonflux](https://github.com/mceq-project/daemonflux) and [MCEq](https://github.com/afedynitch/MCEq), to calculate surface fluxes, and [PROPOSAL](https://github.com/tudo-astroparticlephysics/PROPOSAL), to simulate the propagation of muons through rock and water. 6 | 7 | ## Installation 8 | 9 | MUTE can be installed via pip: 10 | 11 | ``` 12 | pip install mute 13 | ``` 14 | 15 | This will install most of the requirements, including MCEq. 16 | 17 | ### Additional Requirements 18 | 19 | In order to generate custom survival probability tensors, PROPOSAL should be installed. Because it requires compilation, it needs to be installed separately (see detailed installation instructions [here](https://github.com/tudo-astroparticlephysics/PROPOSAL/blob/master/INSTALL.md)): 20 | 21 | ``` 22 | pip install proposal==7.6.2 23 | ``` 24 | 25 | MUTE has been tested with PROPOSAL v7.6.2; earlier or later versions are not guaranteed to work. Environment set-up help for PROPOSAL installation on a Mac can be found [here](docs/Installing_PROPOSAL_on_a_Mac.md). 26 | 27 | ## Getting Started 28 | 29 | ![Total Underground Flux vs Vertical Depth](docs/total_underground_flux.png) 30 | 31 | The code for the above plot is found in [``/examples/example_total_flux_plot.ipynb``](examples/example_total_flux_plot.ipynb). 32 | 33 | For a basic example and a detailed description of how to use MUTE, see the [Tutorial](docs/Tutorial.md). For further examples, see the [examples](examples). 34 | 35 | ## Citation 36 | 37 | Please cite https://inspirehep.net/literature/1927720 for MUTE v1-2 and https://inspirehep.net/literature/2799258 for MUTE v3. 38 | 39 | The current version of MUTE can be cited with the [Zenodo DOI](https://zenodo.org/record/5791812). 40 | 41 | The citations for the models and propagation tools used by MUTE can be found in the [MCEq](https://github.com/afedynitch/MCEq#please-cite-our-work) and [PROPOSAL](https://github.com/tudo-astroparticlephysics/PROPOSAL#how-to-cite-proposal) documentation. 42 | 43 | ### Authors 44 | 45 | [William Woodley](mailto:wwoodley@ualberta.ca) 46 | 47 | ### Contributors 48 | 49 | Anatoli Fedynitch ([@afedynitch](https://github.com/afedynitch)) and Marie-Cécile Piro ([@MarieCecile](https://github.com/MarieCecile)) 50 | 51 | ## License 52 | 53 | MUTE is licensed under the BSD 3-Clause License (see [LICENSE](LICENSE)). -------------------------------------------------------------------------------- /docs/Tutorial_Cluster.md: -------------------------------------------------------------------------------- 1 | # **Tutorial - Using MUTE on a Computer Cluster** 2 | 3 | For high-statistics simulations on a computer cluster, the ``mtp.propagation_muons()`` function is the most useful. The MUTE code to set up a job array of Monte Carlo simulations for 1000 muons per energy-slant depth bin (per job) can be written as follows: 4 | 5 | ```python 6 | # Import packages 7 | 8 | import argparse 9 | 10 | import mute.constants as mtc 11 | import mute.propagation as mtp 12 | 13 | # Parse the job array number from the command line 14 | 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument("job_array_number", type = int) 17 | args = parser.parse_args() 18 | 19 | # Set the constants 20 | 21 | mtc.set_verbose(0) 22 | mtc.set_output(True) 23 | mtc.set_directory("mute/data") 24 | mtc.set_lab("Example") 25 | mtc.set_n_muon(1000) 26 | 27 | # Propagate the muons 28 | 29 | mtp.propagate_muons(seed = args.job_array_number, job_array_number = args.job_array_number, force = True) 30 | ``` 31 | 32 | To prevent any unnecessary output, the verbosity can be set to ``0`` (though it might be a good idea to keep it at the default ``2`` to help figure out what went wrong if the job fails). To make it easier to access and ``sftp`` out the output Monte Carlo underground energy files, the directory can be changed to the working directory of the code or elsewhere. The ``force`` parameter in ``mtc.propagate_muons()`` should be set to ``True`` to force the creation of any required directories or files so the program does not hang, waiting for user input until the job times out. 33 | 34 | The seed, which can be changed in the propagation function to ensure the Monte Carlo results are different for each job, and the job array number can be both read in from the command line using ``argparse``. If the job array consisted of 100 jobs, the survival probabilities can then be calculated as below, setting the ``n_job`` argument in the ``mtp.calc_survival_probability_tensor()`` function to ``100``: 35 | 36 | ```python 37 | import mute.constants as mtc 38 | import mute.propagation as mtp 39 | 40 | mtc.set_lab("Example") 41 | mtc.set_n_muon(100000) 42 | 43 | mtp.calc_survival_probability_tensor(n_job = 100) 44 | ``` 45 | 46 | Here, ``seed`` does not need to be set in ``mtp.calc_survival_probability_tensor()`` because this function will not invoke ``mtp.propagate_muons()``, since the underground energies have already been loaded. Note also that the number of muons was set to 1000 when running the propagation, but is set to 100000 in the code just above. By setting ``n_job`` to 100, MUTE will recognise that the 100000 muons were split evenly between 100 jobs of 1000 muons each, and will search for underground energy files corresponding to 1000 muons. -------------------------------------------------------------------------------- /src/mute/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from shutil import copytree, rmtree 3 | 4 | mute_message = """************************************************************************* 5 | * * 6 | * ███████████████████████████████████████ * 7 | * ▓ ▓▓▓▓ ▓▓ ▓▓▓▓ ▓▓ ▓▓ ▓ * 8 | * ▓ ▓▓ ▓▓ ▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓ * 9 | * ▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒ * 10 | * ▒ ▒ ▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ * 11 | * ░ ░░░░ ░░░░ ░░░░░░░ ░░░░░ ░ * 12 | * ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ * 13 | * https://github.com/wjwoodley/mute * 14 | * * 15 | * Author: William Woodley * 16 | * Version: 3.0.0 * 17 | * * 18 | * Please cite: * 19 | * - https://inspirehep.net/literature/1927720 * 20 | * - https://inspirehep.net/literature/2799258 * 21 | * - The models used for daemonflux, MCEq, PROPOSAL, and mountain maps * 22 | * * 23 | *************************************************************************""" 24 | 25 | print(mute_message) 26 | 27 | GitHub_data_file = "data_20250524" 28 | 29 | 30 | def download_file(url, dir_out): 31 | """Download the data files from GitHub.""" 32 | 33 | import requests 34 | from zipfile import ZipFile 35 | 36 | # Download the zip file 37 | 38 | response = requests.get(url, stream=True) 39 | file_out = os.path.join(dir_out, "mute_data_files.zip") 40 | 41 | with open(file_out, "wb") as f: 42 | 43 | for chunk in response.iter_content(chunk_size=1024 * 1024): 44 | 45 | f.write(chunk) 46 | 47 | # Unzip the file 48 | 49 | with ZipFile(file_out, "r") as f: 50 | 51 | f.extractall(dir_out) 52 | 53 | # If the "data" directory already exists, move the unzipped files into it and replace existing ones with the same name 54 | # If it does not, rename the unzipped directory "data" 55 | 56 | extracted_path = os.path.join(dir_out, GitHub_data_file) 57 | data_path = os.path.join(dir_out, "data") 58 | 59 | if os.path.isdir(data_path): 60 | 61 | copytree(extracted_path, data_path, dirs_exist_ok=True) 62 | rmtree(extracted_path) 63 | 64 | else: 65 | 66 | os.rename(extracted_path, data_path) 67 | 68 | # Delete the zip file 69 | 70 | if os.path.isfile(file_out): 71 | os.remove(file_out) 72 | 73 | 74 | # Download the data files from GitHub to the directory MUTE is being run from if needed 75 | 76 | _data_initialised = False 77 | 78 | 79 | def initialise_data(): 80 | 81 | global _data_initialised 82 | 83 | if _data_initialised: 84 | 85 | return 86 | 87 | else: 88 | 89 | if not os.path.isfile( 90 | os.path.join(os.path.dirname(__file__), "data", f"{GitHub_data_file}.txt") 91 | ): 92 | 93 | download_file( 94 | f"https://github.com/wjwoodley/mute/releases/download/0.1.0/{GitHub_data_file}.zip", 95 | os.path.dirname(__file__), 96 | ) 97 | 98 | _data_initialised = True 99 | 100 | 101 | initialise_data() 102 | -------------------------------------------------------------------------------- /tests/test_spectra.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | 5 | import mute.constants as mtc 6 | import mute.underground as mtu 7 | 8 | 9 | def test_u_ang_dist(): 10 | 11 | mtc.clear() 12 | mtc.set_verbose(0) 13 | mtc.set_overburden("mountain") 14 | mtc.set_reference_density(2.72) 15 | mtc.load_mountain("LNGS") 16 | 17 | u_ang_dist_calc = mtu.calc_u_ang_dist( 18 | kind="zenith", model="daemonflux", output=False, force=True 19 | ) 20 | u_ang_dist_read = [ 21 | 6.42399522e-08, 22 | 7.25018960e-08, 23 | 7.76332714e-08, 24 | 7.98038570e-08, 25 | 8.12272487e-08, 26 | 8.25206082e-08, 27 | 8.42350073e-08, 28 | 8.56617277e-08, 29 | 8.62606174e-08, 30 | 8.65087861e-08, 31 | 8.64939871e-08, 32 | 8.61543533e-08, 33 | 8.54046645e-08, 34 | 8.42920415e-08, 35 | 8.29468845e-08, 36 | 8.12907313e-08, 37 | 7.94858933e-08, 38 | 7.76985731e-08, 39 | 7.58408747e-08, 40 | 7.40547038e-08, 41 | 7.23566774e-08, 42 | 7.07557421e-08, 43 | 6.92502616e-08, 44 | 6.77114598e-08, 45 | 6.60821074e-08, 46 | 6.44274060e-08, 47 | 6.27448673e-08, 48 | 6.09284457e-08, 49 | 5.90056721e-08, 50 | 5.69732639e-08, 51 | 5.48218545e-08, 52 | 5.20426889e-08, 53 | 4.95452738e-08, 54 | 4.71579086e-08, 55 | 4.47989437e-08, 56 | 4.26063072e-08, 57 | 4.04454564e-08, 58 | 3.84773645e-08, 59 | 3.64277875e-08, 60 | 3.39877107e-08, 61 | 3.20296551e-08, 62 | 3.03030113e-08, 63 | 2.86393129e-08, 64 | 2.70709181e-08, 65 | 2.54815616e-08, 66 | 2.38717045e-08, 67 | 2.21676554e-08, 68 | 2.05315886e-08, 69 | 1.90784027e-08, 70 | 1.78905803e-08, 71 | 1.68830541e-08, 72 | 1.59045780e-08, 73 | 1.51336310e-08, 74 | 1.43851377e-08, 75 | 1.36525889e-08, 76 | 1.29081225e-08, 77 | 1.22098946e-08, 78 | 1.14772167e-08, 79 | 1.07749901e-08, 80 | 1.01391551e-08, 81 | 9.56324722e-09, 82 | 9.04692024e-09, 83 | 8.56103965e-09, 84 | 8.09118573e-09, 85 | 7.67895696e-09, 86 | 7.26852767e-09, 87 | 6.82832537e-09, 88 | 6.51004131e-09, 89 | 6.30791477e-09, 90 | 6.08189528e-09, 91 | 5.90182309e-09, 92 | 5.69344604e-09, 93 | 5.49932565e-09, 94 | 5.30590558e-09, 95 | 5.11502935e-09, 96 | 4.90112977e-09, 97 | 4.72291788e-09, 98 | 4.56300730e-09, 99 | 4.38191380e-09, 100 | 4.16571018e-09, 101 | 3.87914611e-09, 102 | 3.61612872e-09, 103 | 3.33314750e-09, 104 | 3.06503834e-09, 105 | 2.72151613e-09, 106 | 2.43635863e-09, 107 | 2.18302028e-09, 108 | 1.91749312e-09, 109 | 1.65878692e-09, 110 | 1.39399764e-09, 111 | 1.18666988e-09, 112 | 1.01019382e-09, 113 | 8.14472690e-10, 114 | 6.33539451e-10, 115 | 4.91731717e-10, 116 | 3.60941251e-10, 117 | 2.73959779e-10, 118 | 2.07981482e-10, 119 | 1.52470179e-10, 120 | 1.00562875e-10, 121 | ] 122 | 123 | assert np.allclose(u_ang_dist_calc, u_ang_dist_read) 124 | 125 | 126 | def test_u_e_spect(): 127 | 128 | mtc.clear() 129 | 130 | u_e_spect_calc = mtu.calc_u_e_spect(model="daemonflux", output=False, force=True) 131 | u_e_spect_read = [ 132 | 0.00000000e00, 133 | 9.32613342e-13, 134 | 2.90264821e-12, 135 | 3.94677811e-12, 136 | 4.59987603e-12, 137 | 4.86706076e-12, 138 | 5.08736771e-12, 139 | 5.00266157e-12, 140 | 5.06006026e-12, 141 | 4.87930498e-12, 142 | 4.82987705e-12, 143 | 4.65524666e-12, 144 | 4.51098817e-12, 145 | 4.39174551e-12, 146 | 4.26546480e-12, 147 | 4.18933164e-12, 148 | 4.09355959e-12, 149 | 3.92054028e-12, 150 | 3.85283423e-12, 151 | 3.70344988e-12, 152 | 3.58540617e-12, 153 | 3.40899736e-12, 154 | 3.28588872e-12, 155 | 3.10054987e-12, 156 | 2.94943248e-12, 157 | 2.75057457e-12, 158 | 2.54182832e-12, 159 | 2.29267365e-12, 160 | 2.02953408e-12, 161 | 1.84540862e-12, 162 | 1.62789127e-12, 163 | 1.27635004e-12, 164 | 9.36877206e-13, 165 | 8.15121290e-13, 166 | 6.05599205e-13, 167 | 3.87345148e-13, 168 | 3.00956625e-13, 169 | 1.74764762e-13, 170 | 1.16314468e-13, 171 | 6.86638693e-14, 172 | 3.77371821e-14, 173 | 2.10798986e-14, 174 | 1.11742269e-14, 175 | 5.64037327e-15, 176 | 2.76758955e-15, 177 | 1.32536065e-15, 178 | 6.22873854e-16, 179 | 2.86849143e-16, 180 | 1.30026653e-16, 181 | 5.81850984e-17, 182 | 2.57474884e-17, 183 | 1.13097340e-17, 184 | 4.92032085e-18, 185 | 2.13213979e-18, 186 | 9.19388086e-19, 187 | 3.94427819e-19, 188 | 1.68892333e-19, 189 | 7.21157779e-20, 190 | 3.06380187e-20, 191 | 1.29963462e-20, 192 | 5.49742315e-21, 193 | 2.31710338e-21, 194 | 9.72085136e-22, 195 | 4.07655136e-22, 196 | 1.70100031e-22, 197 | 7.09861194e-23, 198 | 2.96459291e-23, 199 | 1.23943036e-23, 200 | 5.20817849e-24, 201 | 2.20880159e-24, 202 | 9.49876013e-25, 203 | 4.15868172e-25, 204 | 1.85821243e-25, 205 | 8.46218134e-26, 206 | 3.93773360e-26, 207 | 1.85502475e-26, 208 | 8.86142661e-27, 209 | 4.26268379e-27, 210 | 2.06694082e-27, 211 | 1.00119444e-27, 212 | 4.85208585e-28, 213 | 2.34529700e-28, 214 | 1.12721809e-28, 215 | 5.37343842e-29, 216 | 2.52404021e-29, 217 | 1.15372061e-29, 218 | 4.99680290e-30, 219 | 1.89864499e-30, 220 | 4.83876246e-31, 221 | 0.00000000e00, 222 | 0.00000000e00, 223 | ] 224 | 225 | assert np.allclose(u_e_spect_calc, u_e_spect_read) 226 | -------------------------------------------------------------------------------- /docs/Tutorial_Models.md: -------------------------------------------------------------------------------- 1 | # **Tutorial - Changing the Models** 2 | 3 | ## Table of Contents 4 | 5 | 1. [Primary Model](#primary-model) 6 | 2. [Interaction Model](#interaction-model) 7 | 3. [Atmospheric Model](#atmospheric-model) 8 | 9 | ## Primary Model 10 | 11 | The default primary cosmic ray flux model is GlobalSplineFitBeta, set with the ``primary_model`` keyword argument as ``"gsf"``. The following primary models are available to be set with a string: 12 | 13 | | Model | How to Use in MUTE | Alternatively | Reference File | 14 | |:---------------------------------|:----------------------|:---------------------------------------|:---------------| 15 | | **GlobalSplineFitBeta** | ``"gsf"`` | ``(pm.GlobalSplineFitBeta, None)`` | ``"surface_fluxes_USStd_None_sibyll23c_gsf.txt"`` 16 | | **HillasGaisser2012 (H3a)** | ``"hg"`` or ``"h3a"`` | ``(pm.HillasGaisser2012, "H3a")`` | ``"surface_fluxes_USStd_None_sibyll23c_h3a.txt"`` 17 | | **HillasGaisser2012 (H4a)** | ``"h4a"`` | ``(pm.HillasGaisser2012, "H4a")`` | ``"surface_fluxes_USStd_None_sibyll23c_h4a.txt"`` 18 | | **GaisserHonda** | ``"gh"`` | ``(pm.GaisserHonda, None)`` | ``"surface_fluxes_USStd_None_sibyll23c_gh.txt"`` 19 | | **GaisserStanevTilav (3-gen)** | ``"gst3"`` | ``(pm.GaisserStanevTilav, "3-gen")`` | ``"surface_fluxes_USStd_None_sibyll23c_gst3.txt"`` 20 | | **GaisserStanevTilav (4-gen)** | ``"gst4"`` | ``(pm.GaisserStanevTilav, "4-gen")`` | ``"surface_fluxes_USStd_None_sibyll23c_gst4.txt"`` 21 | | **ZatsepinSokolskaya (Default)** | ``"zs"`` | ``(pm.ZatsepinSokolskaya, "default")`` | ``"surface_fluxes_USStd_None_sibyll23c_zs.txt"`` 22 | | **ZatsepinSokolskaya (PAMELA)** | ``"zsp"`` | ``(pm.ZatsepinSokolskaya, "pamela")`` | ``"surface_fluxes_USStd_None_sibyll23c_zsp.txt"`` 23 | | **SimplePowerlaw27** | ``"pl27"`` | ``(pm.SimplePowerlaw27, None)`` | ``"surface_fluxes_USStd_None_sibyll23c_pl27.txt"`` 24 | 25 | Note that the default primary model is ``"sibyll23c"``, though this can be changed (see [below](#interaction-model)). 26 | 27 | To calculate underground intensities for GaisserHonda, for example, one can do: 28 | 29 | ```python 30 | mtu.calc_u_intensities(method = "sd", primary_model = "gh") 31 | ``` 32 | 33 | Alternatively, in the ``mts.calc_s_fluxes()`` function, the primary model may be set using a tuple. This gives access to the rest of the models available in MCEq. For example: 34 | 35 | ```python 36 | import crflux.models as pm 37 | 38 | s_fluxes = mts.calc_s_fluxes(primary_model = (pm.GaisserStanevTilav, "3-gen")) 39 | 40 | mtu.calc_u_intensities(method = "sd", s_fluxes = s_fluxes) 41 | ``` 42 | 43 | This option is only available in the ``mts.calc_s_fluxes()`` function. The other loading and calculation functions require the primary model to be specified with one of the strings in the list above, as they will search for files with names that contain the strings. 44 | 45 | For more information, see the MCEq Documentation and the crflux Documentation. For an example, see [``/examples/example_primary_flux_models.ipynb``](../examples/example_primary_flux_models.ipynb). 46 | 47 | ## Interaction Model 48 | 49 | The default hadronic interaction model is SIBYLL-2.3c, set with the ``interaction_model`` keyword argument as ``"sibyll23c"``. The following hadronic interaction models are available: 50 | 51 | | Model | How to Use in MUTE | Reference File | 52 | |:-------------------------------|:------------------------|:---------------| 53 | | **DDM** | ``"ddm"`` | ``"surface_fluxes_USStd_None_ddm_gsf.txt"`` 54 | | **DDM Positive Error** | ``"ddm_err_pos"`` | ``"surface_fluxes_USStd_None_ddm_err_pos_gsf.txt"`` 55 | | **DDM Negative Error** | ``"ddm_err_neg"`` | ``"surface_fluxes_USStd_None_ddm_err_neg_gsf.txt"`` 56 | | **SIBYLL-2.3d** | ``"sibyll23d"`` | ``"surface_fluxes_USStd_None_sibyll23d_gsf.txt"`` 57 | | **SIBYLL-2.3d Positive Error** | ``"sibyll23d_err_pos"`` | ``"surface_fluxes_USStd_None_sibyll23d_err_pos_gsf.txt"`` 58 | | **SIBYLL-2.3d Negative Error** | ``"sibyll23d_err_neg"`` | ``"surface_fluxes_USStd_None_sibyll23d_err_neg_gsf.txt"`` 59 | | **SIBYLL-2.3c** | ``"sibyll23c"`` | ``"surface_fluxes_USStd_None_sibyll23c_gsf.txt"`` 60 | | **SIBYLL-2.3c03** | ``"sibyll23c03"`` | ``"surface_fluxes_USStd_None_sibyll23c03_gsf.txt"`` 61 | | **SIBYLL-2.3pp** | ``"sibyll23pp"`` | ``"surface_fluxes_USStd_None_sibyll23pp_gsf.txt"`` 62 | | **SIBYLL-2.3** | ``"sibyll23"`` | ``"surface_fluxes_USStd_None_sibyll23_gsf.txt"`` 63 | | **SIBYLL-2.1** | ``"sibyll21"`` | ``"surface_fluxes_USStd_None_sibyll21_gsf.txt"`` 64 | | **EPOS-LHC** | ``"eposlhc"`` | ``"surface_fluxes_USStd_None_eposlhc_gsf.txt"`` 65 | | **QGSJet-II-04** | ``"qgsjetii04"`` | ``"surface_fluxes_USStd_None_qgsjetii04_gsf.txt"`` 66 | | **QGSJet-II-03** | ``"qgsjetii03"`` | ``"surface_fluxes_USStd_None_qgsjetii03_gsf.txt"`` 67 | | **QGSJet-01c** | ``"qgsjet01c"`` | ``"surface_fluxes_USStd_None_qgsjet01c_gsf.txt"`` 68 | | **DPMJET-III-3.0.6** | ``"dpmjetiii306"`` | ``"surface_fluxes_USStd_None_dpmjetiii306_gsf.txt"`` 69 | | **DPMJET-III-19.1** | ``"dpmjetiii191"`` | ``"surface_fluxes_USStd_None_dpmjetiii191_gsf.txt"`` 70 | 71 | Note that the uncertainties for ``"ddm"`` and ``"sibyll23d"`` are only available using the files provided by MUTE from GitHub for the default primary model ``"gsf"``; MCEq cannot calculate new matrices for these uncertainties. Additionally, new ``"ddm"`` matrices cannot be calculated through MUTE, but can be generated using MCEq (see [``DDM_example.ipynb``](https://github.com/mceq-project/mceq-examples/blob/main/DDM_example.ipynb)) and passed into any ``mute.underground`` function using the ``s_fluxes`` parameter. Note as well that the default primary model is ``"gsf"``, though this can be changed (see [above](#primary-model)) 72 | 73 | To calculate underground intensities for EPOS-LHC, for example, one can do: 74 | 75 | ```python 76 | mtu.calc_u_intensities(method = "sd", interaction_model = "eposlhc") 77 | ``` 78 | 79 | For more information, see the MCEq Documentation. 80 | 81 | ## Atmospheric Model 82 | 83 | The default atmosphere is US Standard Atmosphere. Using the default is equivalent to running: 84 | 85 | ```python 86 | mtu.calc_u_intensities(method = "sd", model = "mceq", atmosphere = "corsika", location = "USStd", month = None) 87 | ``` 88 | 89 | In order to calculate underground fluxes and intensities (and thus the surface fluxes) at a given location, ``atmosphere`` must be set to ``"msis00"`` (the NRLMSISE-00 Model is used to calculate seasonal variations of the atmsophere). ``month`` must also be set to one of the months of the year as a string (``"January"``, ``"February"``, etc.). 90 | 91 | Note that the MCEq models ``"ddm"`` and ``"sibyll23d"`` are not available for different atmospheric models; the most recent working interaction model is ``"sibyll23c"``. 92 | 93 | The following locations are available: 94 | 95 | | Location | How to Use in MUTE | Coordinates | 96 | |:---------------------------|:-------------------|:------------| 97 | | **US Standard Atmosphere** | ``"USStd"`` | N/A 98 | | **South Pole** | ``"SouthPole"`` | ``(-90.00, 0.0)`` 99 | | **Karlsruhe** | ``"Karlsruhe"`` | ``(49.00, 8.4)`` 100 | | **Geneva** | ``"Geneva"`` | ``(46.20, 6.1)`` 101 | | **Tokyo** | ``"Tokyo"`` | ``(35.00, 139.0)`` 102 | | **Gran Sasso** | ``"SanGrasso"`` | ``(42.40, 13.5)`` 103 | | **Tel Aviv** | ``"TelAviv"`` | ``(32.10, 34.8)`` 104 | | **Kennedy Space Centre** | ``"KSC"`` | ``(32.10, -80.7)`` 105 | | **Soudan Mine** | ``"SoudanMine"`` | ``(47.80, -92.2)`` 106 | | **Tsukuba** | ``"Tsukuba"`` | ``(36.20, 140.1)`` 107 | | **Lynn Lake** | ``"LynnLake"`` | ``(56.90, -101.1)`` 108 | | **Peace River** | ``"PeaceRiver"`` | ``(56.15, -117.2)`` 109 | | **Ft Sumner** | ``"FtSumner"`` | ``(34.50, -104.2)`` 110 | | **Lake Baikal** | ``"LakeBaikal"`` | ``(51.60, 103.91)`` 111 | | **P-ONE** | ``"P-ONE"`` | ``(47.90, -127.7)`` 112 | | **KM3NeT-ARCA** | ``"KM3NeT-ARCA"`` | ``(36.6827, 15.1322)`` 113 | | **KM3NeT-ORCA** | ``"KM3NeT-ORCA"`` | ``(42.80, 6.0)`` 114 | | **TRIDENT** | ``"TRIDENT"`` | ``(17.30, 114.0)`` 115 | 116 | Note that because these location strings are passed into MCEq directly, they are case-sensitive, unlike other model parameters. 117 | 118 | As of [v3.0.0](https://github.com/wjwoodley/mute/releases/tag/3.0.0), arbitrary locations can also be passed into calculation functions using a tuple for the ``location`` argument. The following are two equivalent ways of calculating underground intensities for Tokyo in July: 119 | 120 | ```python 121 | mtu.calc_u_intensities(method = "sd", model = "mceq", atmosphere = "msis00", location = "Tokyo", month = "July") 122 | mtu.calc_u_intensities(method = "sd", model = "mceq", atmosphere = "msis00", location = (35.00, 139.0), month = "July") 123 | ``` 124 | 125 | Additional location strings specified by ``(longitude, latitude, altitude)`` coordinates can also be added by editing the MCEq source code at line 29 of `nrlmsise00_mceq.py` and lines 1438 and 642 of `density_profiles.py`. 126 | 127 | Month names are given in the variables ``mtc.MONTHS`` and ``mtc.MONTHS_SNAMES``, which are, respectively: 128 | 129 | ```python 130 | ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] 131 | ``` 132 | ```python 133 | ["Jan.", "Feb.", "Mar.", "Apr.", "May.", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."] 134 | ``` 135 | 136 | For an example of calculations of seasonal variations at the surface and underground, see [``/examples/example_seasonal_variations.ipynb``](../examples/example_seasonal_variations.ipynb). -------------------------------------------------------------------------------- /docs/Tutorial_Labs.md: -------------------------------------------------------------------------------- 1 | # **Tutorial - Modelling Labs** 2 | 3 | ## Table of Contents 4 | 5 | 1. [Laboratory Locations](#laboratory-locations) 6 | 1. [Rock Types](#rock-types) 7 | 2. [Mountain Maps](#mountain-maps) 8 | 9 | ## Laboratory Locations 10 | 11 | The following table provides the locations of the labs used for the total flux plot in [``/examples/example_total_flux_plot.ipynb``](../examples/example_total_flux_plot.ipynb) for reference. These coordinates can be passed to the ``location`` parameter when the ``atmosphere`` parameter is set to ``msis00`` (see [``Tutorial_Models``](Tutorial_Models.md#atmospheric-model)) to specify the atmosphere for the specific location of the lab. 12 | 13 | | Laboratory | Coordinates | How to Use in MUTE | 14 | |:------------|:-------------------|:-------------------| 15 | | **WIPP** | (32.372, -103.794) | ``location = (32.372, -103.794)`` 16 | | **Y2L** | (38.010, 128.543) | ``location = (38.010, 128.543)`` 17 | | **Soudan** | (47.823, -92.237) | ``location = (47.823, -92.237)`` 18 | | **Kamioka** | (36.423, 137.315) | ``location = (36.423, 137.315)`` 19 | | **Boulby** | (54.553, -0.825) | ``location = (54.553, -0.825)`` 20 | | **SUPL** | (-37.070, 142.810) | ``location = (-37.070, 142.810)`` 21 | | **LNGS** | (42.400, 13.500) | ``location = (42.400, 13.500)`` 22 | | **LSM** | (45.179, 6.689) | ``location = (45.179, 6.689)`` 23 | | **SURF** | (44.353, -103.744) | ``location = (44.353, -103.744)`` 24 | | **SNOLAB** | (46.472, -81.187) | ``location = (46.472, -81.187)`` 25 | | **CJPL-I** | (28.153, 101.711) | ``location = (28.153, 101.711)`` 26 | 27 | ## Rock Types 28 | 29 | New in MUTE [v3.0.0](https://github.com/wjwoodley/mute/releases/tag/3.0.0) is the ability to model various types of rock in addition to standard rock. Rock types can be set by setting the reference density with the ``mtc.set_density()`` function to a specific value. The density is associated with $\langle Z\rangle$ and $\langle A\rangle$ values. The available rock types for given laboratories are listed in the table below. 30 | 31 | | Lab or Medium | Density | $\langle Z\rangle$ | $\langle A\rangle$ | Medium | How to Use in MUTE | Reference File | 32 | |:------------------|:--------|:-------------------|:-------------------|:--------------|:-------------------|:---------------| 33 | | **Standard Rock** | 2.65 | 11.00  | 22.00 | Rock [Link] | ``mtc.set_medium("rock")``
``mtc.set_reference_density(2.65)`` | ``"rock_2.65_1000000_survival_probabilities.npy"`` | 34 | | **WIPP** | 2.3 | 14.00  | 29.25 | Salt [Link] | ``mtc.set_medium("salt")``
``mtc.set_reference_density(2.3)`` | ``"salt_2.3_1000000_survival_probabilities.npy"`` | 35 | | **Y2L** [1] | 2.7 | 11.79  | 23.79 | Y2L Rock | ``mtc.set_medium("y2l_rock")``
``mtc.set_reference_density(2.7)`` | ``"y2l_rock_2.7_1000000_survival_probabilities.npy"`` | 36 | | **Soudan** | 2.85 | 12.32  | 24.90 | Rock | `mtc.set_medium("rock")`
`mtc.set_reference_density(2.85)` | ``"rock_2.85_1000000_survival_probabilities.npy"`` | 37 | | **Kamioka** | 2.7 | 11.31  | 22.76 | Rock | ``mtc.set_medium("rock")``
``mtc.set_reference_density(2.7)`` | ``"rock_2.7_1000000_survival_probabilities.npy"`` | 38 | | **Boulby** | 2.62 | 11.70  | 23.60 | Rock | ``mtc.set_medium("rock")``
``mtc.set_reference_density(2.62)`` | ``"rock_2.62_1000000_survival_probabilities.npy"`` | 39 | | **SUPL** [2] | - | - | - | Rock | ``mtc.set_medium("rock")``
``mtc.set_reference_density(2.65)`` | ``"rock_2.65_1000000_survival_probabilities.npy"`` | 40 | | **LNGS** | 2.72 | 11.42  | 22.83 | Rock | ``mtc.set_medium("rock")``
``mtc.set_reference_density(2.72)`` | `"rock_2.72_1000000_survival_probabilities.npy"` | 41 | | **LSM** | 2.73 | 11.74  | 23.48 | Fréjus Rock [Link]| ``mtc.set_medium("frejus_rock")``
``mtc.set_reference_density(2.73)`` | ``"frejus_rock_2.73_1000000_survival_probabilities.npy"`` | 42 | | **SURF** | 2.86 | 12.01  | 23.98 | Rock | ``mtc.set_medium("rock")``
``mtc.set_reference_density(2.86)`` | ``"rock_2.86_1000000_survival_probabilities.npy"`` | 43 | | **SNOLAB** | 2.83 | 12.02  | 24.22 | Rock | ``mtc.set_medium("rock")``
``mtc.set_reference_density(2.83)`` | ``"rock_2.83_1000000_survival_probabilities.npy"`` | 44 | | **CJPL-I** | 2.8 | 12.15  | 24.30 | Rock | ``mtc.set_medium("rock")``
``mtc.set_reference_density(2.8)`` | ``"rock_2.8_1000000_survival_probabilities.npy"`` | 45 | | **Water or Ice** | 0.997 | 7.33  | 14.43 | Water [Link] | ``mtc.set_medium("water")``
``mtc.set_reference_density(0.997)`` | ``"water_0.997_1000000_survival_probabilities.npy"`` | 46 | | **ANTARES Water** | 1.03975 | 7.42  | 14.76 | ANTARES Water [Link] | ``mtc.set_medium("antares_water")``
``mtc.set_reference_density(1.03975)`` | ``"antares_water_1.03975_1000000_survival_probabilities.npy"`` | 47 | 48 | ``` 49 | [1]: Because the rocks above Y2L and Kamioka both have an average density of 2.7 gcm^-3, Y2L rock must be 50 | specified by "y2l_rock" rather than "rock" like all other rock types. This does not imply a different 51 | set of Sternheimer parameters; "y2l_rock" uses the Sternheimer parameters of standard rock. 52 | 53 | [2]: Because and values are not available for SUPL, the transfer tensor for standard rock is used. 54 | ``` 55 | 56 | Setting the medium as described above tells MUTE to reference the listed reference file, which contains a pre-calculated and supplied survival probability tensor that was calculated for the specified $\langle Z\rangle$ and $\langle A\rangle$ values. These values for each medium as listed in Table II of and in the table above were set in the PROPOSAL source code for component defintions in line 285 of ``Components.cxx`` in order to generate these transfer tensors. 57 | 58 | Details on chemical composition and Sternheimer parameters for each medium as listed in Table VI of are given in the PROPOSAL source code for medium definitions here. 59 | 60 | ## Mountain Maps 61 | 62 | Also new in MUTE [v3.0.0](https://github.com/wjwoodley/mute/releases/tag/3.0.0) is the supply of mountain map files for multiple laboratories under mountains. Each mountain map is centered around a specific detector in the given lab in order to provide an origin to the coordinate system used in the map. Maps are typically produced from satallite data centered on the (latitude, longitude) coordinates of the detector, or from detector data. As MUTE requires the mountain map files to be in a specific format in order to be compatible with the ``mtc.load_mountain()`` function, and as some maps are difficult to find or not available in the literature, this is a convenient way of quickly loading the maps for calculations for one or multiple laboratories. The available maps and how to load them are listed in the table below. 63 | 64 | | Lab | Detector | How to Use in MUTE | Reference File | Citations | 65 | |:----|:---------|:-------------------|:---------------|:----------| 66 | | **Y2L** [1] | COSINE-100 |``mtc.set_medium("y2l_rock")``
``mtc.set_reference_density(2.7)``
``mtc.load_mountain("Y2L")``|``y2l_mountain.txt``| | 67 | | **Kamioka** | Super-Kamiokande |``mtc.set_medium("rock")``
``mtc.set_reference_density(2.7)``
``mtc.load_mountain("SuperK")``|``superk_mountain.txt``|S. Abe et al. (KamLAND), Phys. Rev. C 81, 025807 (2010).

Y. Fukuda et al. (Super-Kamiokande), Nucl. Instrum. Meth. A 501, 418 (2003).

Digital Map 50 m Grid (Elevation), Geographical Survey Institute of Japan (1997). | 68 | | **Kamioka** | KamLAND |``mtc.set_medium("rock")``
``mtc.set_reference_density(2.7)``
``mtc.load_mountain("KamLAND")``|``kamland_mountain.txt``|S. Abe et al. (KamLAND), Phys. Rev. C 81, 025807 (2010).

Digital Map 50 m Grid (Elevation), Geographical Survey Institute of Japan (1997). | 69 | | **LNGS** | LVD |``mtc.set_medium("rock")``
``mtc.set_reference_density(2.72)``
``mtc.load_mountain("LNGS")``|``lngs_mountain.txt``|M. Aglietta et al. (LVD), Phys. Rev. D 58, 092005 (1998).| 70 | | **LSM** | Fréjus |``mtc.set_medium("frejus_rock")``
``mtc.set_reference_density(2.73)``
``mtc.load_mountain("LSM")``|``lsm_mountain.txt``|C. Berger et al. (Fréjus), Phys. Rev. D 40, 2163 (1989). | 71 | | **CJPL-I** | JNE |``mtc.set_medium("rock")``
``mtc.set_reference_density(2.8)``
``mtc.load_mountain("CJPL")``|``cjpl_mountain.txt``|Z. Guo et al. (JNE), Chin. Phys. C 45, 025001 (2021). | 72 | 73 | ``` 74 | [1]: The slant depth values for the first 27 azimuthal bins of theta = 0 in this map have been averaged to 75 | a value of 1.67094153 km.w.e. (previously a minimum of 0.66349473 km.w.e. and a maximum of 76 | 0.68703341 km.w.e.) due to issues with these bins. This change lowers the total underground flux by 77 | 0.02%, which is now (4.72379 ± 0.10664)e-7 cm^-2 s^-1, while it was previously 78 | (4.72495 ± 0.10665)e-7 cm^-2 s^-1. 79 | ``` 80 | 81 | We request on behalf of the collaborations that have kindly shared their maps that you please take care to cite the citations provided in the table if you make use of any of these maps in your work. 82 | 83 | Note that, for these given mountain maps, the string is case-insensitive, meaning ``mtc.load_mountain("Y2L")`` and ``mtc.load_mountain("y2l")`` will load the same map. For user-provided paths, the string is not case-insensitive. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # **Changelog** 2 | 3 | ## [3.0.0](https://github.com/wjwoodley/mute/releases/tag/3.0.0) - 24 May 2025 4 | 5 | ### **New Features** 6 | 7 | * **daemonflux:** [daemonflux](https://github.com/mceq-project/daemonflux) can now be used from within the MUTE framework to calculate suface fluxes. Because either daemonflux or MCEq can be used, a new argument ``model`` has been added to ``surface`` and ``underground`` functions to specify which is to be used (default: ``mceq``; in the next release, the default will become ``daemonflux``). 8 | * **New Functions:** A number of new functions can now be used. 9 | * **``mts.calc_s_e_spect()``:** Calculates surface energy spectra. 10 | * **``mtu.calc_u_ang_dist()``:** Calculates underground zenith or azimuthal angular distributions (for mountain overburdens only, since the distributions are trivial for flat overburdens). 11 | * **``mtu.calc_u_e_spect()``:** Calculates underground energy spectra. 12 | * **``mtu.calc_u_mean_e()``:** Calculates underground mean and median energies, as well as upper and lower 68% and 95% confidence intervals for the median. 13 | * **``mtu.calc_depth()``:** Calculates various depths (equivalent vertical, straight vertical, minimum, maximum, and average) for mountain overburdens. 14 | * A number of new examples have been added to [``/examples``](examples) to demonstrate use of these functions. 15 | * **Provided Mountain Maps:** Mountain maps for various laboratories are now provided (Y2L, Super-Kamiokande, KamLAND, LSM, LNGS, and CJPL). See [``Tutorial_Labs``](/docs/Tutorial_Labs.md) for information on use. 16 | * **New Propagation Media:** A number of new propagation medium options, corresponding to different underground and underwater laboratories, are now available in addition to standard rock. ANTARES water is now available as a propagation medium. The ice propagation medium is now an alias for (fresh) water. New transfer tensors for these media are provided in the supplied data files. See [``Tutorial_Labs``](/docs/Tutorial_Labs.md) for information on use. Air is no longer available as a propagation medium. 17 | * **Location Coordinates:** Locations for MCEq can now be set by passing in (latitude, longitude) tuples (in degrees) into the ``location`` parameter in any function. 18 | * **Loading Mountains:** New parameters are available when loading mountains, including the ``scale`` parameter, which can be used to scale all slant depths in the loaded mountain map by some ratio (useful for calculating uncertainties, for example). 19 | * **Loading Surface Fluxes with Files:** Surface fluxes can now be loaded from user-named files by passing the file name into the ``file_name`` parameter in the ``mts.load_s_fluxes_from_file()`` function. 20 | 21 | ### **Bug Fixes** 22 | 23 | * **Intensity Calculation:** A bug in the calculation of intensities for mountains was corrected using a new calculation method. The new results differ from those returned by MUTE v2 by less than 7% at the deepest depths. 24 | * **Loading Transfer Tensors:** Transfer tensors can now be loaded successfully when passing a file name to ``mtp.load_survival_probability_tensor_from_file()``. 25 | * **NumPy and SciPy:** MUTE function calls to NumPy and SciPy have been updated to correspond with the latest versions of these libraries. 26 | 27 | ### **Other Changes** 28 | 29 | * **Number of Muons:** The default value for ``n_muon`` is now ``1e6`` instead of ``1e5``. New transfer tensors for 1e6 muons are provided in the supplied data files. 30 | * **Lowercase Letters:** Most elements of all file names, as well as some arguments, are now lowercase. Interaction models also now have their dashes and periods removed if passed into functions with dashes and periods. This is backwards-compatible. For example, user-input ``"SIBYLL-2.3d"`` is now turned into ``"sibyll23d"`` in all cases. Note that the location arguments, including ``"USStd"``, are case-sensitive, as are month arguments, including ``None``, as these arguments are passed directly into MCEq. New file names in lowercase are provided in the supplied data files, and old files have been removed (though will persist in the data directory if MUTE is updated over a previous installation). 31 | * **Vertical Depths for Mountains:** An exception is now thrown when attempting to set a vertical depth while the overburden type is set to mountain. This alerts the user that they are possibly in a different overburden mode from intended. 32 | * **Changing the Overburden Type:** When the overburden type is changed, the set and loaded constants specific to flat or mountain overburdens are now reset (meaning, if, for example, the overburden type is ``"flat"`` and the vertical depth is set to 3.0 km.w.e., then the overburden type is changed to ``"mountain"``, then changed back to ``"flat"``, the vertical depth will be reset to 0.5 km.w.e.). 33 | * **Transfer Tensor Files:** Transfer tensors are now stored in ``.npy`` files instead of plain text files in order to save disk space. 34 | * **Documentation:** The tutorial has now been split up into different files for better organisation and easier navigation. 35 | * **ASCII Art:** The ``__init__.py`` file now prints ASCII art when a MUTE module is imported for the first time. 36 | * **Default Interaction Model:** In the [previous release of MUTE](https://github.com/wjwoodley/mute/releases/tag/2.0.1), it was stated that the default interaction model would be changed from SIBYLL-2.3c to DDM. Instead, the default interaction model remains SIBYLL-2.3c. Although SIBYLL-2.3d and DDM are now available to be used in MCEq, SIBYLL-2.3c is still the most recent model for which all features in MUTE are available, particularly calculating surface fluxes with the NRLMSISE-00 atmosphere in order to specify location and month. 37 | 38 | ### **Deprecations and Upcoming Changes** 39 | 40 | * **Energy Units:** In the next release (v3.1.0), the energy units in all cases will change from [MeV] to [GeV]. 41 | * **Setting Densities:** The ``mtc.set_density()`` and ``mtc.get_density()`` functions have been renamed ``mtc.set_reference_density()`` and ``mtc.get_reference_density()`` respectively. The former will be removed in the next release (v3.1.0). 42 | * **Default Surface Flux Model:** In the next release (v3.1.0), the default surface flux model set by the ``model`` parameter will be changed to ``daemonflux`` (it is currently ``"mceq"``). 43 | * **Hillas-Gaisser Model Parameters:** The ``"hg"`` primary model option has been split into ``"h3a"`` and ``"h4a"`` to refer to the three- and four-component Hillas-Gaisser primary flux models. The former is now deprecated and will be removed in the next release (v3.1.0). This is to conform to naming conventions within the cosmic ray community. 44 | * **File Name Parameters:** In the next release (v3.1.0), file name parameters previously called ``file_name`` will be called ``input_file`` and ``output_file`` to remove ambiguity. 45 | 46 | ## [2.0.1](https://github.com/wjwoodley/mute/releases/tag/2.0.1) - 17 November 2022 47 | 48 | ### **Bug Fixes** 49 | 50 | * **PROPOSAL Version:** The suggested PROPOSAL version has been updated to v7.4.2. The previously-suggested v7.1.1 sometimes returns an error when calculating new transfer tensors. 51 | 52 | ### **Other Changes** 53 | 54 | * **Intensities Method:** The ``method`` argument is now an optional argument in the ``mtu.calc_u_intensities()`` function. The default for flat overburdens is ``"sd"``, and the default for mountains is ``"dd"``. Other methods can still be specified as normal, and this should not change any existing scripts. See the [Tutorial](docs/Tutorial.md#calculating-underground-intensities) or the function docstrings for more information. 55 | * **Default Interaction Model:** In the next release of MUTE, the default interaction model will be changed from SIBYLL-2.3c to DDM. 56 | 57 | ## [2.0.0](https://github.com/wjwoodley/mute/releases/tag/2.0.0) - 15 July 2022 58 | 59 | ### **New Features** 60 | 61 | * **Mountains:** Calculations can now be done for non-flat overburdens. The ``calc_u_fluxes()`` function returns a three-dimensional tensor when the overburden type is set to ``"mountain"`` (or when the new ``full_tensor`` argument is set to ``True``). [An example](examples/example_mountain_calculations.ipynb) has been added to show this. 62 | * **Intensity Functions:** Intensities are now calculated with the ``mtu.calc_u_intensities()`` function by passing the ``method`` argument. The previous functions, ``mtu.calc_u_intensities_tr()`` and ``mtu.calc_u_intensities_eq()`` have been removed. 63 | * **Surface Calculations:** The functions ``mts.calc_s_intensities()`` and ``mts.calc_s_tot_fluxes()`` have been added for calculations of surface intensities and total fluxes. 64 | * **Calculations from Pre-Calculated Matrices:** Underground fluxes, intensities, and total fluxes can now be calculated from underground flux matrices, surface flux matrices, and survival probability tensors that are already defined in the code by passing them directly into the function with the ``u_fluxes``, ``s_fluxes``, and ``survival_probability_tensor`` arguments. The old interface of specifying the models and atmospheres is also still available. 65 | * **Energy Threshold:** An energy threshold can now be specified by setting the ``E_th`` argument in ``mtu.calc_u_intensities()`` and ``mtu.calc_u_tot_fluxes`` to a value in MeV. 66 | * **File Names:** Custom input and output file names can now be used in all function by specifying the optional ``file_name`` argument. If the argument is not given, the default file name will be used. If ``output`` is set to ``False`` (either globally with ``mtc.set_output(False)`` or in the function call), no output will be written to the file, and the file name will be ignored. The previously-used ``file_name`` argument in ``mtp.calc_survival_probability_tensor()`` has been renamed ``file_name_pattern``. 67 | * **Primary Models:** The SimplePowerlaw27 model can now be set with ``"PL27"``. 68 | * **Months Variables:** Constants called ``MONTHS`` and ``MONTHS_SNAMES`` have been added for ease of looping through the months of the year. [An example](examples/example_seasonal_variations.ipynb) has been added to show this. 69 | 70 | ### **Bug Fixes** 71 | 72 | * **Downloading Data Files:** The zip folder containing the data files that is downloaded from GitHub is now properly deleted after unzipping. 73 | * **Joining Paths:** File paths are now joined using ``os.path.join()``, rather than using string concatenation. 74 | * **Closing Files:** Some underground calculation functions previously did not properly close the output files after writing the results. 75 | * **Survival Probability Cache:** The survival probability tensor is now only reloaded or re-calculated if the global propagation constants are changed, rather than every time a function is called. 76 | * **Default Tracking:** Default tracking is now turned off in MCEq, improving computation time of surface fluxes. 77 | 78 | ### **Other Changes** 79 | 80 | * **Argument Order:** The order of arguments in the surface and underground functions has been changed to make them more sensical and consistent. See the [Tutorial](docs/Tutorial.md) or the docstrings of individual functions for more information. 81 | * **Slant Depths:** The default slant depths now go from 0.5 to 14 km.w.e. instead of 1 to 12 km.w.e. Values for slant depths outside this range are possible to calculate with the default transfer tensors by setting ``mtc.shallow_extrapolation`` to ``True``, but the results are not guaranteed to be good. 82 | * **Underground Fluxes Return:** The return of ``mtu.calc_u_fluxes()`` is now one single tensor, not a tuple of two matrices. 83 | * **Angles in Underground Fluxes:** ``mtu.calc_u_fluxes()`` no longer takes an ``angles`` argument. The tensor is always returned for the default angles given by ``mtc._ANGLES``. 84 | * **Energy Grid:** The energy grid can now only be accessed with ``mtc.ENERGIES``, not ``mtc.energies``. 85 | * **Surface Output Files:** The ``surface_fluxes`` directory in ``mute/data/`` has been renamed ``surface`` for more generality. The surface flux file names now contain the month as well (or ``None``) if no month is set. 86 | * **Print Grid Functions:** The functions that display the grids used in the output files have been removed. 87 | 88 | ## [1.0.1](https://github.com/wjwoodley/mute/releases/tag/1.0.1) - 24 December 2021 89 | 90 | ### Bug Fixes 91 | 92 | * **Total Flux Function:** Unnecessary lines in the ``mtu.calc_u_tot_flux()`` function which were causing an error have been removed. 93 | * **Vertical Depth Restrictions:** The restrictions in the ``mtc.set_vertical_depth()`` function for the depth to be between 1 km.w.e. and 12 km.w.e. were removed, due to lack of ensured consistency with a loaded survival probability tensor. 94 | * **Surface Flux Test File:** Fixed a bug where the directory was not being set properly to find the test data file. 95 | 96 | ### Other Changes 97 | 98 | * **Total Flux Test File:** A test file [``test_total_flux.py``](mute/tests/test_total_flux.py) has been added to test the calculation of the total fluxes. 99 | * **Zenodo DOI:** The Zenodo DOI has been included in the [README.md](README.md) for citation of different versions of the code. 100 | 101 | ## [1.0.0](https://github.com/wjwoodley/mute/releases/tag/1.0.0) - 19 December 2021 102 | 103 | ### New Features 104 | 105 | * **Energy Cuts:** Energies above ~100 PeV are now excluded to make the Monte Carlo and calculations more efficient. 106 | * **Primary Models:** Zatsepin-Sokolskaya models can now be set with ``"ZS"`` and ``"ZSP"``. Additional primary models can now be specified by passing a tuple for the ``primary_model`` argument in the ``mts.calc_s_fluxes()`` function, as is done in MCEq. 107 | * **Air:** Air can now be used as a medium of propagation (with ``mtc.set_medium("air")``). 108 | * **File Names:** An argument has been added to specify alternative file name patterns when loading underground energies from a file with ``mtp.calc_survival_probability_tensor()``. 109 | * **Spline:** A spline function has been added to make calculations of surface fluxes more efficient, and to improve calculations of underground fluxes. The surface fluxes are now calculated for a smaller number of zenith angles, making the MCEq calculations quicker as well. 110 | 111 | ### Bug Fixes 112 | 113 | * **Number of Muons:** Fixed consistency issue with definition of ``n_muon`` between ``mtp._load_u_energies_from_files()`` and ``mtp.calc_survival_probability_tensor()``. 114 | * **Data Directory:** The data files from GitHub are now downloaded to the directory set with ``mtc.set_directory()``. 115 | * **Test Files:** Fixed surface fluxes and underground intensity test files. 116 | 117 | ### Other Changes 118 | 119 | * **Integrals:** Integration is now done using the more accurate ``scipy.integrate.simpson()`` function, instead of ``np.trapz()``. 120 | * **Surface Fluxes Test:** A ``test`` argument has been added to ``mts.calc_s_fluxes()`` which runs only three zenith angles when testing the surface fluxes. 121 | * **Disk Storage:** Underground energies are now stored as binary files instead of literal ASCII files, saving disk space and increasing efficiency of both outputting and loading in underground energies (30 MB to 18 MB, and 20 minutes to 4 minutes). 122 | * **Memory:** The underground energies are now loaded in a more memory-efficient way. The intermediate ``mtu.load_u_energies_from_files()`` function no longer needs to be run. -------------------------------------------------------------------------------- /examples/example_underground_flux.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# **Example: Calculating Underground Fluxes**\n", 8 | "\n", 9 | "This file demonstrates how to use MUTE to calculate underground fluxes for a lab located 3.7 km.w.e. under rock.\n", 10 | "\n", 11 | "## Import Packages" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [ 19 | { 20 | "name": "stdout", 21 | "output_type": "stream", 22 | "text": [ 23 | "*************************************************************************\n", 24 | "* *\n", 25 | "* ███████████████████████████████████████ *\n", 26 | "* ▓ ▓▓▓▓ ▓▓ ▓▓▓▓ ▓▓ ▓▓ ▓ *\n", 27 | "* ▓ ▓▓ ▓▓ ▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓ ▓▓▓▓▓▓ *\n", 28 | "* ▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒ *\n", 29 | "* ▒ ▒ ▒ ▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ *\n", 30 | "* ░ ░░░░ ░░░░ ░░░░░░░ ░░░░░ ░ *\n", 31 | "* ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ *\n", 32 | "* https://github.com/wjwoodley/mute *\n", 33 | "* *\n", 34 | "* Author: William Woodley *\n", 35 | "* Version: 3.0.0 *\n", 36 | "* *\n", 37 | "* Please cite: *\n", 38 | "* - https://inspirehep.net/literature/1927720 *\n", 39 | "* - https://inspirehep.net/literature/2799258 *\n", 40 | "* - The models used for daemonflux, MCEq, PROPOSAL, and mountain maps *\n", 41 | "* *\n", 42 | "*************************************************************************\n" 43 | ] 44 | } 45 | ], 46 | "source": [ 47 | "import numpy as np\n", 48 | "\n", 49 | "import mute.constants as mtc\n", 50 | "import mute.surface as mts\n", 51 | "import mute.propagation as mtp\n", 52 | "import mute.underground as mtu" 53 | ] 54 | }, 55 | { 56 | "cell_type": "markdown", 57 | "metadata": {}, 58 | "source": [ 59 | "## Set the Constants" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 2, 65 | "metadata": {}, 66 | "outputs": [], 67 | "source": [ 68 | "mtc.set_verbose(2)\n", 69 | "mtc.set_output(True)\n", 70 | "mtc.set_lab(\"Example\")\n", 71 | "mtc.set_overburden(\"flat\")\n", 72 | "mtc.set_vertical_depth(3.7)\n", 73 | "mtc.set_medium(\"rock\")\n", 74 | "mtc.set_reference_density(2.65)\n", 75 | "mtc.set_n_muon(1000000)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "## Check the Slant Depths\n", 83 | "\n", 84 | "Check the number and value of the slant depths that the underground fluxes will be calculated with. Because the vertical depth was set to ``3.7`` above, the slant depths should start at 3.7 km.w.e. The number of slant depths should be reduced from the default 28 to 22." 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 3, 90 | "metadata": {}, 91 | "outputs": [ 92 | { 93 | "name": "stdout", 94 | "output_type": "stream", 95 | "text": [ 96 | "[ 3.7 4. 4.5 5. 5.5 6. 6.5 7. 7.5 8. 8.5 9. 9.5 10.\n", 97 | " 10.5 11. 11.5 12. 12.5 13. 13.5 14. ]\n", 98 | "22\n" 99 | ] 100 | } 101 | ], 102 | "source": [ 103 | "print(mtc.slant_depths)\n", 104 | "print(len(mtc.slant_depths))" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "The underground fluxes will be calculated for the zenith angles corresponding to these slant depths. The correspondence is given by this equation:\n", 112 | "\n", 113 | "$$\\theta=\\arccos\\left(\\frac{h}{X}\\right)=\\arccos\\left(\\frac{3.7\\ \\mathrm{km.w.e.}}{X}\\right)$$\n", 114 | "\n", 115 | "## Calculate the Underground Fluxes\n", 116 | "\n", 117 | "The ``mtu.calc_u_fluxes()`` function will return a matrix of shape ``(91, 22)`` for the 91 energies in the grid given by ``mtc.ENERGIES``, and the 22 angles corresponding to the slant depths above." 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 4, 123 | "metadata": {}, 124 | "outputs": [ 125 | { 126 | "name": "stdout", 127 | "output_type": "stream", 128 | "text": [ 129 | "Calculating underground fluxes.\n", 130 | "Loading surface fluxes for daemonflux.\n", 131 | "Loaded surface fluxes.\n", 132 | "Loading survival probabilities from data/survival_probabilities/rock_2.65_1000000_survival_probabilities.npy.\n", 133 | "Loaded survival probabilities.\n", 134 | "Finished calculating underground fluxes.\n", 135 | "Underground fluxes written to data/underground/Example_underground_fluxes.txt.\n", 136 | "[[0.00000000e+00 0.00000000e+00 0.00000000e+00 ... 0.00000000e+00\n", 137 | " 0.00000000e+00 0.00000000e+00]\n", 138 | " [6.16703202e-11 2.87100319e-12 7.85940435e-13 ... 2.15525490e-17\n", 139 | " 1.40345948e-17 1.03211958e-17]\n", 140 | " [1.90158930e-10 7.66299041e-12 2.48824130e-12 ... 9.69024934e-17\n", 141 | " 5.98686426e-17 2.71763838e-17]\n", 142 | " ...\n", 143 | " [1.76953790e-30 1.01423372e-30 5.28797216e-31 ... 0.00000000e+00\n", 144 | " 0.00000000e+00 0.00000000e+00]\n", 145 | " [8.06786863e-31 3.64688148e-31 8.43004699e-32 ... 0.00000000e+00\n", 146 | " 0.00000000e+00 0.00000000e+00]\n", 147 | " [1.95098752e-31 2.78689621e-34 0.00000000e+00 ... 0.00000000e+00\n", 148 | " 0.00000000e+00 0.00000000e+00]]\n", 149 | "(91, 22)\n" 150 | ] 151 | } 152 | ], 153 | "source": [ 154 | "u_fluxes = mtu.calc_u_fluxes(model = \"daemonflux\")\n", 155 | "\n", 156 | "print(u_fluxes)\n", 157 | "print(u_fluxes.shape)" 158 | ] 159 | }, 160 | { 161 | "cell_type": "markdown", 162 | "metadata": {}, 163 | "source": [ 164 | "If the full tensor is needed, this can be obtained by setting ``full_tensor`` to ``True`` in the function call. This will return a three-dimensional array of shape ``(28, 91, 20)``, for the default slant depths, energies, and surface flux zenith angles given by ``mtc._SLANT_DEPTHS``, ``mtc.ENERGIES``, and ``mtc.ANGLES_FOR_S_FLUXES`` respectively. The output file will also contain the full tensor." 165 | ] 166 | }, 167 | { 168 | "cell_type": "code", 169 | "execution_count": 5, 170 | "metadata": {}, 171 | "outputs": [ 172 | { 173 | "name": "stdout", 174 | "output_type": "stream", 175 | "text": [ 176 | "Calculating underground fluxes.\n", 177 | "Loading surface fluxes for daemonflux.\n", 178 | "Loaded surface fluxes.\n", 179 | "Survival probabilities already loaded for rock with density 2.65 gcm^-3 and 1000000 muons.\n", 180 | "Finished calculating underground fluxes.\n", 181 | "Underground fluxes written to data/underground/Example_underground_fluxes.txt.\n", 182 | "[[0.00000000e+00 0.00000000e+00 0.00000000e+00 0.00000000e+00\n", 183 | " 0.00000000e+00]\n", 184 | " [6.16703202e-11 6.17509618e-11 6.19923356e-11 6.23927929e-11\n", 185 | " 6.29516409e-11]\n", 186 | " [1.90158930e-10 1.90406375e-10 1.91147018e-10 1.92375801e-10\n", 187 | " 1.94090450e-10]\n", 188 | " [2.57498340e-10 2.57834063e-10 2.58838938e-10 2.60506102e-10\n", 189 | " 2.62832552e-10]\n", 190 | " [2.81240649e-10 2.81606777e-10 2.82702660e-10 2.84520812e-10\n", 191 | " 2.87057890e-10]]\n", 192 | "(28, 91, 20)\n" 193 | ] 194 | } 195 | ], 196 | "source": [ 197 | "u_fluxes_full = mtu.calc_u_fluxes(full_tensor = True, model = \"daemonflux\")\n", 198 | "\n", 199 | "print(u_fluxes_full[0, :5, :5])\n", 200 | "print(u_fluxes_full.shape)" 201 | ] 202 | }, 203 | { 204 | "cell_type": "markdown", 205 | "metadata": {}, 206 | "source": [ 207 | "## Calculating Fluxes from Pre-Calculated Matrices\n", 208 | "\n", 209 | "If the code already has a surface flux matrix and / or survival probability tensor defined, they can be passed directly into the ``mtu.calc_u_fluxes()`` function. This is especially useful when dealing with multiple surface flux matrices and / or survival probability tensors in the same file, like when calculating various quantities by looping over variables. For example, to calculate both the surface fluxes and underground fluxes for different locations, the following can be done:" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": 6, 215 | "metadata": {}, 216 | "outputs": [ 217 | { 218 | "name": "stdout", 219 | "output_type": "stream", 220 | "text": [ 221 | "Survival probabilities already loaded for rock with density 2.65 gcm^-3 and 1000000 muons.\n", 222 | "Calculating surface fluxes for SoudanMine using gsf and sibyll23c.\n", 223 | "MCEqRun::set_interaction_model(): SIBYLL23C\n" 224 | ] 225 | }, 226 | { 227 | "name": "stderr", 228 | "output_type": "stream", 229 | "text": [ 230 | ".local/lib/python3.12/site-packages/crflux/models.py:1068: DeprecationWarning: Please import `InterpolatedUnivariateSpline` from the `scipy.interpolate` namespace; the `scipy.interpolate.fitpack2` namespace is deprecated and will be removed in SciPy 2.0.0.\n", 231 | " self.p_frac_spl, self.p_flux_spl, self.n_flux_spl = pickle.load(\n" 232 | ] 233 | }, 234 | { 235 | "name": "stdout", 236 | "output_type": "stream", 237 | "text": [ 238 | "MCEqRun::set_density_model(): Setting density profile to CORSIKA ('BK_USStd', None)\n", 239 | "MCEqRun::set_primary_model(): GlobalSplineFitBeta \n", 240 | "MCEqRun::set_density_model(): Setting density profile to MSIS00 ('SoudanMine', 'January')\n" 241 | ] 242 | }, 243 | { 244 | "name": "stderr", 245 | "output_type": "stream", 246 | "text": [ 247 | "100%|██████████| 20/20 [01:03<00:00, 3.17s/it]\n" 248 | ] 249 | }, 250 | { 251 | "name": "stdout", 252 | "output_type": "stream", 253 | "text": [ 254 | "Finished calculating surface fluxes.\n", 255 | "Surface fluxes written to data/surface/surface_fluxes_SoudanMine_January_sibyll23c_gsf.txt.\n", 256 | "Calculating underground fluxes.\n", 257 | "Finished calculating underground fluxes.\n", 258 | "Underground fluxes written to data/underground/Example_underground_fluxes.txt.\n", 259 | "Calculating surface fluxes for SanGrasso using gsf and sibyll23c.\n", 260 | "MCEqRun::set_interaction_model(): SIBYLL23C\n" 261 | ] 262 | }, 263 | { 264 | "name": "stderr", 265 | "output_type": "stream", 266 | "text": [ 267 | ".local/lib/python3.12/site-packages/crflux/models.py:1068: DeprecationWarning: Please import `InterpolatedUnivariateSpline` from the `scipy.interpolate` namespace; the `scipy.interpolate.fitpack2` namespace is deprecated and will be removed in SciPy 2.0.0.\n", 268 | " self.p_frac_spl, self.p_flux_spl, self.n_flux_spl = pickle.load(\n" 269 | ] 270 | }, 271 | { 272 | "name": "stdout", 273 | "output_type": "stream", 274 | "text": [ 275 | "MCEqRun::set_density_model(): Setting density profile to CORSIKA ('BK_USStd', None)\n", 276 | "MCEqRun::set_primary_model(): GlobalSplineFitBeta \n", 277 | "MCEqRun::set_density_model(): Setting density profile to MSIS00 ('SanGrasso', 'January')\n" 278 | ] 279 | }, 280 | { 281 | "name": "stderr", 282 | "output_type": "stream", 283 | "text": [ 284 | "100%|██████████| 20/20 [01:03<00:00, 3.15s/it]\n" 285 | ] 286 | }, 287 | { 288 | "name": "stdout", 289 | "output_type": "stream", 290 | "text": [ 291 | "Finished calculating surface fluxes.\n", 292 | "Surface fluxes written to data/surface/surface_fluxes_SanGrasso_January_sibyll23c_gsf.txt.\n", 293 | "Calculating underground fluxes.\n", 294 | "Finished calculating underground fluxes.\n", 295 | "Underground fluxes written to data/underground/Example_underground_fluxes.txt.\n", 296 | "Calculating surface fluxes for Tokyo using gsf and sibyll23c.\n", 297 | "MCEqRun::set_interaction_model(): SIBYLL23C\n" 298 | ] 299 | }, 300 | { 301 | "name": "stderr", 302 | "output_type": "stream", 303 | "text": [ 304 | ".local/lib/python3.12/site-packages/crflux/models.py:1068: DeprecationWarning: Please import `InterpolatedUnivariateSpline` from the `scipy.interpolate` namespace; the `scipy.interpolate.fitpack2` namespace is deprecated and will be removed in SciPy 2.0.0.\n", 305 | " self.p_frac_spl, self.p_flux_spl, self.n_flux_spl = pickle.load(\n" 306 | ] 307 | }, 308 | { 309 | "name": "stdout", 310 | "output_type": "stream", 311 | "text": [ 312 | "MCEqRun::set_density_model(): Setting density profile to CORSIKA ('BK_USStd', None)\n", 313 | "MCEqRun::set_primary_model(): GlobalSplineFitBeta \n", 314 | "MCEqRun::set_density_model(): Setting density profile to MSIS00 ('Tokyo', 'January')\n" 315 | ] 316 | }, 317 | { 318 | "name": "stderr", 319 | "output_type": "stream", 320 | "text": [ 321 | "100%|██████████| 20/20 [01:03<00:00, 3.18s/it]" 322 | ] 323 | }, 324 | { 325 | "name": "stdout", 326 | "output_type": "stream", 327 | "text": [ 328 | "Finished calculating surface fluxes.\n", 329 | "Surface fluxes written to data/surface/surface_fluxes_Tokyo_January_sibyll23c_gsf.txt.\n", 330 | "Calculating underground fluxes.\n", 331 | "Finished calculating underground fluxes.\n", 332 | "Underground fluxes written to data/underground/Example_underground_fluxes.txt.\n" 333 | ] 334 | }, 335 | { 336 | "name": "stderr", 337 | "output_type": "stream", 338 | "text": [ 339 | "\n" 340 | ] 341 | } 342 | ], 343 | "source": [ 344 | "locations = [\"SoudanMine\", \"SanGrasso\", \"Tokyo\"]\n", 345 | "\n", 346 | "all_s_fluxes = []\n", 347 | "all_u_fluxes = []\n", 348 | "\n", 349 | "sp_tensor = mtp.load_survival_probability_tensor_from_file()\n", 350 | "\n", 351 | "for loc in locations:\n", 352 | " \n", 353 | " s_fluxes = mts.calc_s_fluxes(atmosphere = \"msis00\", location = loc, month = \"January\")\n", 354 | " u_fluxes = mtu.calc_u_fluxes(s_fluxes = s_fluxes, survival_probability_tensor = sp_tensor)\n", 355 | " \n", 356 | " all_s_fluxes.append(s_fluxes)\n", 357 | " all_u_fluxes.append(u_fluxes)" 358 | ] 359 | }, 360 | { 361 | "cell_type": "markdown", 362 | "metadata": {}, 363 | "source": [ 364 | "Like this, the loop will not have to be broken to calculate the underground fluxes, and the surface fluxes will not have to be calculated again inside the ``mtu.calc_u_fluxes()`` function like they are in the ``mts.calc_s_fluxes()`` function, because they are being passed into it directly.\n", 365 | "\n", 366 | "The functions for surface intensities and total fluxes can take ``s_fluxes`` matrices in as well, and the functions for underground intensities and total fluxes can take in both ``s_fluxes`` and ``survival_probability_tensor`` arguments." 367 | ] 368 | } 369 | ], 370 | "metadata": { 371 | "kernelspec": { 372 | "display_name": "Python 3 (ipykernel)", 373 | "language": "python", 374 | "name": "python3" 375 | }, 376 | "language_info": { 377 | "codemirror_mode": { 378 | "name": "ipython", 379 | "version": 3 380 | }, 381 | "file_extension": ".py", 382 | "mimetype": "text/x-python", 383 | "name": "python", 384 | "nbconvert_exporter": "python", 385 | "pygments_lexer": "ipython3", 386 | "version": "3.12.5" 387 | } 388 | }, 389 | "nbformat": 4, 390 | "nbformat_minor": 4 391 | } 392 | -------------------------------------------------------------------------------- /src/mute/propagation.py: -------------------------------------------------------------------------------- 1 | ######################### 2 | ######################### 3 | ### ### 4 | ### MUTE ### 5 | ### William Woodley ### 6 | ### 24 May 2025 ### 7 | ### ### 8 | ######################### 9 | ######################### 10 | 11 | # Import packages 12 | 13 | import os 14 | 15 | import numpy as np 16 | from tqdm import tqdm 17 | 18 | import mute.constants as constants 19 | 20 | try: 21 | 22 | import proposal as pp 23 | 24 | except ImportError: 25 | 26 | pass 27 | 28 | # Create the propagator 29 | 30 | 31 | def _create_propagator(force): 32 | """This function creates the propagator object in PROPOSAL for use in propagation._propagation_loop()""" 33 | 34 | # Check values 35 | 36 | constants._check_constants(force=force) 37 | 38 | # The creation of the propagator can be very slow the first time 39 | # The propagator is used in every iteration of the doubly-nested propagation loop 40 | # Make it a global variable so it only has to be created once 41 | 42 | global propagator 43 | 44 | if constants.get_verbose() > 1: 45 | print("Creating propagator.") 46 | 47 | # Propagator arguments 48 | 49 | mu = pp.particle.MuMinusDef() 50 | cuts = pp.EnergyCutSettings(500, 0.05, True) 51 | 52 | if constants.get_medium() == "rock" or constants.get_medium() == "y2l_rock": 53 | 54 | medium = pp.medium.StandardRock() 55 | 56 | elif constants.get_medium() == "frejus_rock": 57 | 58 | medium = pp.medium.FrejusRock() 59 | 60 | elif constants.get_medium() == "salt": 61 | 62 | medium = pp.medium.Salt() 63 | 64 | elif constants.get_medium() == "water" or constants.get_medium() == "ice": 65 | 66 | medium = pp.medium.Water() 67 | 68 | elif constants.get_medium() == "antares_water": 69 | 70 | medium = pp.medium.AntaresWater() 71 | 72 | else: 73 | 74 | raise NotImplementedError( 75 | "Medium type {0} not implemented. The only options are {1}.".format( 76 | constants.get_medium(), constants._accepted_media 77 | ) 78 | ) 79 | 80 | args = {"particle_def": mu, "target": medium, "interpolate": True, "cuts": cuts} 81 | 82 | # Initialise standard cross-sections, then specify and set parametrisation models 83 | 84 | cross_sections = pp.crosssection.make_std_crosssection(**args) 85 | 86 | brems_param = pp.parametrization.bremsstrahlung.KelnerKokoulinPetrukhin(lpm=False) 87 | epair_param = pp.parametrization.pairproduction.KelnerKokoulinPetrukhin(lpm=False) 88 | ionis_param = pp.parametrization.ionization.BetheBlochRossi(energy_cuts=cuts) 89 | shado_param = pp.parametrization.photonuclear.ShadowButkevichMikheyev() 90 | photo_param = pp.parametrization.photonuclear.AbramowiczLevinLevyMaor97( 91 | shadow_effect=shado_param 92 | ) 93 | 94 | cross_sections[0] = pp.crosssection.make_crosssection(brems_param, **args) 95 | cross_sections[1] = pp.crosssection.make_crosssection(epair_param, **args) 96 | cross_sections[2] = pp.crosssection.make_crosssection(ionis_param, **args) 97 | cross_sections[3] = pp.crosssection.make_crosssection(photo_param, **args) 98 | 99 | # Propagation utility 100 | 101 | collection = pp.PropagationUtilityCollection() 102 | 103 | collection.interaction = pp.make_interaction(cross_sections, True) 104 | collection.displacement = pp.make_displacement(cross_sections, True) 105 | collection.time = pp.make_time(cross_sections, mu, True) 106 | collection.decay = pp.make_decay(cross_sections, mu, True) 107 | 108 | pp.PropagationUtilityCollection.cont_rand = False 109 | 110 | utility = pp.PropagationUtility(collection=collection) 111 | 112 | # Other settings 113 | 114 | pp.do_exact_time = False 115 | 116 | # Set up geometry 117 | 118 | detector = pp.geometry.Sphere( 119 | position=pp.Cartesian3D(0, 0, 0), radius=10000000, inner_radius=0 120 | ) 121 | density_distr = pp.density_distribution.density_homogeneous( 122 | mass_density=constants.get_reference_density() 123 | ) 124 | 125 | propagator = pp.Propagator(mu, [(detector, utility, density_distr)]) 126 | 127 | if constants.get_verbose() > 1: 128 | print("Finished creating propagator.") 129 | 130 | return propagator 131 | 132 | 133 | # Propagation function 134 | 135 | 136 | def _propagation_loop(energy, slant_depth, force): 137 | """This function propagates n_muon muons, looping over the energies and slant depths, and returns the muons' underground energies in [MeV].""" 138 | 139 | # Check values 140 | 141 | constants._check_constants(force=force) 142 | 143 | n_muon = constants.get_n_muon() 144 | 145 | # Convert the slant depth from [km.w.e.] to [cm] 146 | 147 | convert_to_cm = 1e5 / constants.get_reference_density() 148 | 149 | # Initialise a list of underground energies 150 | 151 | u_energies_ix = [] 152 | 153 | # Define the initial state of the muon 154 | 155 | mu_initial = pp.particle.ParticleState() 156 | mu_initial.energy = energy + constants._MU_MASS 157 | mu_initial.position = pp.Cartesian3D(0, 0, 0) 158 | mu_initial.direction = pp.Cartesian3D(0, 0, -1) 159 | 160 | # Propagate n_muon muons 161 | 162 | for _ in range(n_muon): 163 | 164 | # Propagate the muons 165 | 166 | track = propagator.propagate(mu_initial, slant_depth * convert_to_cm) 167 | 168 | # Test whether or not the muon has energy left (has not lost all of its energy or has not decayed) 169 | # If it does, record its energy 170 | # If it does not, ignore this muon and proceed with the next loop iteration 171 | 172 | if ( 173 | track.track_energies()[-1] != constants._MU_MASS 174 | and track.track_types()[-1] != pp.particle.Interaction_Type.decay 175 | ): 176 | 177 | # Store the final underground energy of the muon 178 | 179 | u_energies_ix.append(track.track_energies()[-1]) 180 | 181 | # Return the underground energies for the muon 182 | 183 | return u_energies_ix 184 | 185 | 186 | # Propagate the muons and return underground energies 187 | 188 | 189 | def propagate_muons(seed=0, job_array_number=0, output=None, force=False): 190 | """ 191 | Propagate muons for the default surface energy grid and slant depths. 192 | 193 | The default surface energy grid is given by constants.ENERGIES, and the default slant depths are given by constants._SLANT_DEPTHS. 194 | 195 | Parameters 196 | ---------- 197 | seed : int, optional (default: 0) 198 | The random seed for use in the PROPOSAL propagator. 199 | 200 | job_array_number : int, optional (default: 0) 201 | The job array number from a high-statistics run on a computer cluster. This is set so the underground energy files from each job in the job array will be named differently. 202 | 203 | output : bool, optional (default: taken from constants.get_output()) 204 | If True, an output file will be created to store the results. 205 | 206 | force : bool, optional (default: False) 207 | If True, force the creation of new directories if required. 208 | 209 | Returns 210 | ------- 211 | u_energies : NumPy ndarray 212 | A two-dimensional array containing lists of underground energies for muons that survived the propagation. The shape of the array will be (91, 28), and the underground energies will be in units of [MeV]. 213 | """ 214 | 215 | # Check values 216 | 217 | constants._check_constants(force=force) 218 | 219 | if output is None: 220 | output = constants.get_output() 221 | 222 | assert type(job_array_number) == int, "job_array_number must be an integer." 223 | 224 | # Create the propagator once 225 | 226 | _create_propagator(force=force) 227 | 228 | # Set the random seed 229 | 230 | pp.RandomGenerator.get().set_seed(seed) 231 | 232 | # Initialise a matrix of underground energies 233 | 234 | u_energies = np.zeros( 235 | (len(constants.ENERGIES), len(constants._SLANT_DEPTHS)), dtype=np.ndarray 236 | ) 237 | 238 | # Run the propagation function 239 | 240 | if constants.get_verbose() >= 1: 241 | 242 | print( 243 | "Propagating {0} muons.".format( 244 | constants.get_n_muon() 245 | * len(constants.ENERGIES) 246 | * len(constants._SLANT_DEPTHS) 247 | ) 248 | ) 249 | 250 | for i in ( 251 | tqdm(range(len(constants.ENERGIES))) 252 | if constants.get_verbose() >= 1 253 | else range(len(constants.ENERGIES)) 254 | ): 255 | 256 | for x in range(len(constants._SLANT_DEPTHS)): 257 | 258 | u_energies[i, x] = _propagation_loop( 259 | constants.ENERGIES[i], constants._SLANT_DEPTHS[x], force=force 260 | ) 261 | 262 | if constants.get_verbose() >= 1: 263 | print("Finished propagation.") 264 | 265 | # Write the results to the file 266 | 267 | if output: 268 | 269 | constants._check_directory( 270 | os.path.join(constants.get_directory(), "underground_energies"), force=force 271 | ) 272 | 273 | file_name = os.path.join( 274 | constants.get_directory(), 275 | "underground_energies", 276 | "{0}_{1}_{2}_underground_energies_{3}.npy".format( 277 | constants.get_medium(), 278 | constants.get_reference_density(), 279 | constants.get_n_muon(), 280 | job_array_number, 281 | ), 282 | ) 283 | 284 | np.save(file_name, u_energies) 285 | 286 | if constants.get_verbose() > 1: 287 | print(f"Underground energies written to {file_name}.") 288 | 289 | return u_energies 290 | 291 | 292 | # Load underground energies 293 | 294 | 295 | def _load_u_energies_from_files(file_name_pattern, n_job=1, force=False): 296 | """ 297 | Load the underground energies resulting from the PROPOSAL Monte Carlo from a file or collection of files stored in data/underground_energies. 298 | 299 | Parameters 300 | ---------- 301 | file_name_pattern : str 302 | The file name pattern for the file(s) that the underground energy data is stored in. This should end before an underscore so the function can append the job array number. For example, pass "underground_energies" for a set of files beginning with "underground_energies_0.npy". 303 | 304 | n_job : int, optional (default: 1) 305 | The number of jobs that were run on the computer cluster. Set this to the number of files the underground energies are spread across. 306 | 307 | force : bool, optional (default: False) 308 | If True, force the propagation of muons and the creation of new directories if required. 309 | 310 | Returns 311 | ------- 312 | u_energies : NumPy ndarray 313 | A two-dimensional array containing lists of underground energies for muons that survived the propagation. The shape of the array will be (91, 28), and the underground energies will be in units of [MeV]. 314 | """ 315 | 316 | # Check values 317 | 318 | constants._check_constants(force=force) 319 | 320 | # Check that the directory exists 321 | 322 | if not os.path.exists( 323 | os.path.join(constants.get_directory(), "underground_energies") 324 | ): 325 | 326 | if constants.get_verbose() >= 1: 327 | 328 | print( 329 | f"{constants.get_directory}/underground_energies does not exist. Underground energies not loaded." 330 | ) 331 | 332 | return 333 | 334 | # Test if the file exists 335 | 336 | if not os.path.isfile(f"{file_name_pattern}_0.npy"): 337 | 338 | if constants.get_verbose() >= 1: 339 | 340 | print( 341 | f"{file_name_pattern}_0.npy does not exist. Underground energies not loaded." 342 | ) 343 | 344 | return 345 | 346 | # Fill a u_energies array with empty lists that will be able to be extended 347 | 348 | u_energies = np.empty( 349 | (len(constants.ENERGIES), len(constants._SLANT_DEPTHS)), dtype=object 350 | ) 351 | 352 | for i in np.ndindex(u_energies.shape): 353 | u_energies[i] = [] 354 | 355 | # Loop over all output files and add the contents to u_energies 356 | 357 | for job_array_number in ( 358 | tqdm(range(n_job)) if constants.get_verbose() >= 1 else range(n_job) 359 | ): 360 | 361 | u_energies += np.load( 362 | f"{file_name_pattern}_{job_array_number}.npy", allow_pickle=True 363 | ) 364 | 365 | if constants.get_verbose() > 1: 366 | print("Loaded underground energies.") 367 | 368 | return u_energies 369 | 370 | 371 | # Calculate survival probabilities 372 | 373 | 374 | def calc_survival_probability_tensor( 375 | seed=0, file_name_pattern=None, n_job=1, output=None, file_name="", force=False 376 | ): 377 | """ 378 | Calculate survival probabilities in units of [(MeV^2 km.w.e.)^-1] for the default surface energy grid and slant depths. 379 | 380 | The default surface energy grid is given by constants.ENERGIES, and the default slant depths are given by constants._SLANT_DEPTHS. If the propagation of muons has already been done, this will load the underground energies file, unless force is set to True. 381 | 382 | Parameters 383 | ---------- 384 | seed : int, optional (default: 0) 385 | The random seed for use in the PROPOSAL propagator. 386 | 387 | file_name_pattern : str, optional (default: None) 388 | The file name pattern for the file(s) that the underground energy data is stored in. This should end before an underscore so the function can append the job array number. For example, pass "underground_energies" for a set of files beginning with "underground_energies_0.npy". 389 | 390 | n_job : int, optional (default: 1) 391 | The number of jobs that were run on the computer cluster. Set this to the number of files the underground energy data is spread across. 392 | 393 | output : bool, optional (default: taken from constants.get_output()) 394 | If True, an output file will be created to store the results. 395 | 396 | file_name : str, optional (default: constructed from set global propagation constants) 397 | The name of the file in which to store the results. If output is False or None, this is ignored. 398 | 399 | force : bool, optional (default: False) 400 | If True, this will force the muons to be propagated in the Monte Carlo whether an underground energies file already exists or not. 401 | 402 | Returns 403 | ------- 404 | survival_probability_tensor : NumPy ndarray 405 | A three-dimensional array containing the survival probabilities. The shape of the array will be (91, 28, 91), and the survival probabilities will be in units of [(MeV^2 km.w.e.)^-1]. 406 | """ 407 | 408 | # Check values 409 | 410 | if output is None: 411 | output = constants.get_output() 412 | 413 | # Construct a default file name pattern 414 | 415 | file_name_pattern_default = os.path.join( 416 | constants.get_directory(), 417 | "underground_energies", 418 | "{0}_{1}_{2}_underground_energies".format( 419 | constants.get_medium(), 420 | constants.get_reference_density(), 421 | int(constants.get_n_muon() / n_job), 422 | ), 423 | ) 424 | 425 | # Check if propagate_muons() should be forced or not 426 | # If not, check if underground energy files exist 427 | # If not, ask if muons should be propagated 428 | 429 | if force: 430 | 431 | u_energies = propagate_muons(seed=seed, output=output, force=force) 432 | 433 | else: 434 | 435 | # Check if the user has specified underground energies to load 436 | # If not, look for the default file name pattern and check if it exists 437 | # If so, load the underground energies 438 | # If not, ask if muons should be propagated 439 | 440 | if file_name_pattern is not None: 441 | 442 | u_energies = _load_u_energies_from_files( 443 | file_name_pattern=os.path.join( 444 | constants.get_directory(), "underground_energies", file_name_pattern 445 | ), 446 | n_job=n_job, 447 | force=force, 448 | ) 449 | 450 | elif os.path.isfile(f"{file_name_pattern_default}_0.npy"): 451 | 452 | u_energies = _load_u_energies_from_files( 453 | file_name_pattern=file_name_pattern_default, n_job=n_job, force=force 454 | ) 455 | 456 | else: 457 | 458 | answer = input( 459 | "No underground energy file currently exists for the set medium, density, or number of muons. Would you like to create one (y/n)?: " 460 | ) 461 | 462 | if answer.lower() == "y": 463 | 464 | u_energies = propagate_muons(seed=seed, output=output, force=force) 465 | 466 | else: 467 | 468 | print("Underground energies not calculated.") 469 | print("Survival probabilities not calculated.") 470 | 471 | return 472 | 473 | # Check that the underground energies were loaded 474 | 475 | if u_energies is None: 476 | 477 | raise Exception( 478 | "Survival probabilities not calculated. The underground energies were not loaded or calculated correctly." 479 | ) 480 | 481 | # Calculate the survival probabilities 482 | # Zeroth index = Surface energy 483 | # First index = Slant depth 484 | # Second index = Underground energy 485 | 486 | survival_probability_tensor = np.zeros( 487 | (len(constants.ENERGIES), len(constants._SLANT_DEPTHS), len(constants.ENERGIES)) 488 | ) 489 | 490 | if constants.get_verbose() > 1: 491 | print("Calculating survival probabilities.") 492 | 493 | # Loop over the surface energies and slant depths 494 | # Histogram the underground energies to count how many are in each underground energy bin 495 | # Divide the counts by the number of muons 496 | # This counts how many muons survived per bin out of the total number that was thrown 497 | 498 | for i in range(len(constants.ENERGIES)): 499 | 500 | for x in range(len(constants._SLANT_DEPTHS)): 501 | 502 | survival_probability_tensor[i, x, :] = np.histogram( 503 | np.array(u_energies[i, x]), bins=constants._E_BINS 504 | )[0] / float(constants.get_n_muon()) 505 | 506 | if constants.get_verbose() > 1: 507 | print("Finished calculating survival probabilities.") 508 | 509 | # Write the results to the file 510 | 511 | if output: 512 | 513 | constants._check_directory( 514 | os.path.join(constants.get_directory(), "survival_probabilities"), 515 | force=force, 516 | ) 517 | 518 | if file_name == "" or not isinstance(file_name, str): 519 | 520 | file_name = os.path.join( 521 | constants.get_directory(), 522 | "survival_probabilities", 523 | "{0}_{1}_{2}_survival_probabilities.npy".format( 524 | constants.get_medium(), 525 | constants.get_reference_density(), 526 | constants.get_n_muon(), 527 | ), 528 | ) 529 | 530 | np.save(file_name, survival_probability_tensor) 531 | 532 | if constants.get_verbose() > 1: 533 | print(f"Survival probabilities written to {file_name}.") 534 | 535 | return survival_probability_tensor 536 | 537 | 538 | def load_survival_probability_tensor_from_file(file_name="", force=False): 539 | """ 540 | Retrieve a survival probability tensor in units of [(MeV^2 km.w.e.)^-1] stored in data/survival_probabilities based on the set global propagation constants. 541 | 542 | The function searches for a file name that matches the set medium, density, and number of muons. If the file does not exist, prompt the user to run propagation.calc_survival_probability_tensor(). 543 | 544 | Parameters 545 | ---------- 546 | file_name : str, optional (default: constructed from set global propagation constants) 547 | The name of the file in which to store the results. This must be the full path to the file. If output is False or None, this is ignored. 548 | 549 | force : bool 550 | If True, force the calculation of a new survival probability tensor and the creation of new directories if required. 551 | 552 | Returns 553 | ------- 554 | survival_probability_tensor : NumPy ndarray 555 | A three-dimensional array containing the survival probabilities. The shape of the array will be (91, 28, 91), and the survival probabilities will be in units of [(MeV^2 km.w.e.)^-1]. 556 | """ 557 | 558 | # Check values 559 | 560 | constants._check_constants(force=force) 561 | 562 | # Define a function to update the survival probability tensor cache 563 | 564 | def update_cache(survival_probability_tensor): 565 | 566 | constants._survival_probability_tensor_configuration = { 567 | "medium": constants.get_medium(), 568 | "density": constants.get_reference_density(), 569 | "n_muon": constants.get_n_muon(), 570 | } 571 | constants._current_survival_probability_tensor = survival_probability_tensor 572 | 573 | # Define a function to run if there is no survival probability file 574 | 575 | def no_file(force): 576 | 577 | # If the file does not exist, ask the user if they want to run PROPOSAL to create it 578 | 579 | if not force: 580 | 581 | answer = input( 582 | "No survival probability matrix currently exists for the set medium, density, or number of muons. Would you like to create one (y/n)?: " 583 | ) 584 | 585 | if force or answer.lower() == "y": 586 | 587 | survival_probability_tensor = calc_survival_probability_tensor(force=force) 588 | 589 | update_cache(survival_probability_tensor) 590 | 591 | return survival_probability_tensor 592 | 593 | else: 594 | 595 | print("Survival probabilities not calculated.") 596 | 597 | return 598 | 599 | # Construct a file name based on the set medium, density, and number of muons if one has not been provided 600 | # Then update the cache 601 | # If a file name has been provided, use that and do not update the cache 602 | 603 | if file_name == "" or not isinstance(file_name, str): 604 | 605 | # Check if a survival probability tensor has already been loaded 606 | # If so, return the tensor that has already been loaded 607 | # If not, load a tensor based on the set global propagation constants 608 | 609 | if ( 610 | constants._current_survival_probability_tensor is not None 611 | and constants._survival_probability_tensor_configuration 612 | == { 613 | "medium": constants.get_medium(), 614 | "density": constants.get_reference_density(), 615 | "n_muon": constants.get_n_muon(), 616 | } 617 | ): 618 | 619 | if constants.get_verbose() > 1: 620 | print( 621 | "Survival probabilities already loaded for {0} with density {1} gcm^-3 and {2} muons.".format( 622 | constants._survival_probability_tensor_configuration["medium"], 623 | constants._survival_probability_tensor_configuration["density"], 624 | constants._survival_probability_tensor_configuration["n_muon"], 625 | ) 626 | ) 627 | 628 | return constants._current_survival_probability_tensor 629 | 630 | else: 631 | 632 | file_name = os.path.join( 633 | constants.get_directory(), 634 | "survival_probabilities", 635 | "{0}_{1}_{2}_survival_probabilities.npy".format( 636 | constants.get_medium(), 637 | constants.get_reference_density(), 638 | constants.get_n_muon(), 639 | ), 640 | ) 641 | 642 | # Check if the file exists 643 | # If it does, attempt to load the tensor from the file 644 | 645 | if os.path.isfile(file_name): 646 | 647 | if constants.get_verbose() > 1: 648 | print(f"Loading survival probabilities from {file_name}.") 649 | 650 | survival_probability_tensor = np.load(file_name) 651 | 652 | # Assert that the file have the correct numbers of energies and slant depths 653 | 654 | assert survival_probability_tensor.shape == ( 655 | len(constants.ENERGIES), 656 | len(constants._SLANT_DEPTHS), 657 | len(constants.ENERGIES), 658 | ) 659 | 660 | # If so, update the cache 661 | 662 | if constants.get_verbose() > 1: 663 | print("Loaded survival probabilities.") 664 | 665 | update_cache(survival_probability_tensor) 666 | 667 | return survival_probability_tensor 668 | 669 | # If the file does not exist, run the no_file() function 670 | 671 | else: 672 | 673 | return no_file(force=force) 674 | -------------------------------------------------------------------------------- /src/mute/constants.py: -------------------------------------------------------------------------------- 1 | ######################### 2 | ######################### 3 | ### ### 4 | ### MUTE ### 5 | ### William Woodley ### 6 | ### 24 May 2025 ### 7 | ### ### 8 | ######################### 9 | ######################### 10 | 11 | # Import packages 12 | 13 | import os 14 | from collections import namedtuple 15 | import warnings 16 | 17 | import numpy as np 18 | 19 | warnings.simplefilter("always") 20 | 21 | # Energies in [MeV] 22 | 23 | _E_BINS = np.logspace(1.9, 14, 122)[:-30] # Bin edges 24 | _E_WIDTHS = np.diff(_E_BINS) # Bin widths 25 | ENERGIES = np.sqrt(_E_BINS[1:] * _E_BINS[:-1]) # Bin centers 26 | 27 | # Slant depths in [km.w.e.] and angles in [degrees] 28 | # These are the defaults used to construct the matrices 29 | # The user can enter their own angles in the function calls, which will interpolate over the grids created by these angles 30 | 31 | _X_MIN = 0.5 32 | _X_MAX = 14 33 | 34 | _SLANT_DEPTHS = np.linspace(_X_MIN, _X_MAX, int(2 * (_X_MAX - _X_MIN) + 1)) 35 | _ANGLES = np.degrees(np.arccos(_X_MIN / _SLANT_DEPTHS)) 36 | 37 | slant_depths = _SLANT_DEPTHS 38 | angles = _ANGLES 39 | 40 | # Angles in [degrees] specifically for the calculation of surface flux matrices 41 | 42 | ANGLES_FOR_S_FLUXES = np.linspace(0, 90, 20) 43 | 44 | # Other constants 45 | # The rest mass of a muon in [MeV] 46 | # Months of the year 47 | 48 | _MU_MASS = 105.6583745 49 | 50 | MONTHS = [ 51 | "January", 52 | "February", 53 | "March", 54 | "April", 55 | "May", 56 | "June", 57 | "July", 58 | "August", 59 | "September", 60 | "October", 61 | "November", 62 | "December", 63 | ] 64 | MONTHS_SNAMES = [ 65 | "Jan.", 66 | "Feb.", 67 | "Mar.", 68 | "Apr.", 69 | "May.", 70 | "Jun.", 71 | "Jul.", 72 | "Aug.", 73 | "Sep.", 74 | "Oct.", 75 | "Nov.", 76 | "Dec.", 77 | ] 78 | 79 | # Define default user-set global constants 80 | 81 | _verbose = 2 82 | _output = True 83 | _directory = os.path.join(os.path.dirname(__file__), "data") 84 | _lab = "Default" 85 | _overburden = "flat" 86 | _vertical_depth = _X_MIN 87 | _medium = "rock" 88 | _reference_density = 2.65 89 | _n_muon = 1000000 90 | 91 | # Define which media are accepted in MUTE 92 | 93 | _accepted_media = [ 94 | "rock", 95 | "frejus_rock", 96 | "y2l_rock", 97 | "salt", 98 | "water", 99 | "antares_water", 100 | "ice", 101 | ] 102 | 103 | # Keep track of which survival probability tensor is loaded 104 | 105 | _survival_probability_tensor_configuration = {} 106 | _current_survival_probability_tensor = None 107 | 108 | # Keep track of whether or not a mountain profile has been loaded 109 | 110 | _mountain_loaded = False 111 | 112 | # Use extrapolation for depths lower than 0.5 km.w.e. 113 | # This limitation comes from PROPOSAL, not MUTE 114 | 115 | shallow_extrapolation = False 116 | 117 | # Setters and getters for global constants 118 | 119 | # Verbose 120 | 121 | 122 | def set_verbose(verbose): 123 | """ 124 | Set the verbosity level. 125 | 126 | Parameters 127 | ---------- 128 | verbose : int (default: 2) 129 | The verbosity level. Options: 130 | 0 = Print no information. 131 | 1 = Print the progress through the calculation of surface fluxes if calculating surface fluxes, the propagation loop if propagating muons, or the loading of the Monte Carlo underground energies from files if loading underground energies. 132 | 2 = Print when calculations start and finish, and where data is read from and written to. 133 | """ 134 | 135 | assert isinstance(verbose, int) 136 | 137 | global _verbose 138 | 139 | _verbose = verbose 140 | 141 | 142 | def get_verbose(): 143 | """Return the set verbosity level.""" 144 | 145 | return _verbose 146 | 147 | 148 | # Whether or not the results are written to output files in directory 149 | 150 | 151 | def set_output(output): 152 | """ 153 | Set whether results are output to files or not. The default is True. 154 | 155 | Most functions also have their own optional output keyword argument to give more control over which output files are created. 156 | """ 157 | 158 | assert isinstance(output, bool) 159 | 160 | global _output 161 | 162 | _output = output 163 | 164 | 165 | def get_output(): 166 | """Return the set output setting.""" 167 | 168 | return _output 169 | 170 | 171 | # Directory for input and output 172 | 173 | 174 | def set_directory(directory): 175 | """Set the working directory for files to be written to and read from. The default is \"data\".""" 176 | 177 | from .__init__ import download_file, GitHub_data_file 178 | 179 | global _directory 180 | 181 | _directory = directory 182 | 183 | if not os.path.isfile(os.path.join(_directory, "data", f"{GitHub_data_file}.txt")): 184 | 185 | download_file( 186 | f"https://github.com/wjwoodley/mute/releases/download/0.1.0/{GitHub_data_file}.zip", 187 | _directory, 188 | ) 189 | 190 | 191 | def get_directory(): 192 | """Return the set working directory for files to be written to and read from.""" 193 | 194 | return _directory 195 | 196 | 197 | # Lab name used in output files 198 | 199 | 200 | def set_lab(lab): 201 | """Set the name of the lab. This is used in the output file names in the \"underground\" data directory as a way to differentiate different data files. The default is \"Default\".""" 202 | 203 | global _lab 204 | 205 | _lab = lab 206 | 207 | 208 | def get_lab(): 209 | """Return the set name of the lab.""" 210 | 211 | return _lab 212 | 213 | 214 | # Overburden 215 | 216 | 217 | def set_overburden(overburden): 218 | """Set the overburden type. The default is \"flat\" overburden.""" 219 | 220 | assert overburden in [ 221 | "flat", 222 | "mountain", 223 | ], 'overburden must be set to either "flat" or "mountain".' 224 | 225 | global _overburden 226 | 227 | # Clear constants when switching overburden types 228 | 229 | if _overburden == "mountain" and overburden == "flat": 230 | 231 | if get_verbose() > 1: 232 | print( 233 | "Setting overburden to flat and resetting mountain overburden constants." 234 | ) 235 | 236 | global _mountain_zenith_all 237 | global _mountain_azimuthal_all 238 | global _mountain_slant_depths_all 239 | global mountain 240 | 241 | _mountain_zenith_all = None 242 | _mountain_azimuthal_all = None 243 | _mountain_slant_depths_all = None 244 | mountain = None 245 | 246 | del _mountain_zenith_all 247 | del _mountain_azimuthal_all 248 | del _mountain_slant_depths_all 249 | del mountain 250 | 251 | elif _overburden == "flat" and overburden == "mountain": 252 | 253 | if get_verbose() > 1: 254 | print( 255 | "Setting overburden to mountain and resetting flat overburden constants." 256 | ) 257 | 258 | global _vertical_depth 259 | 260 | _vertical_depth = _X_MIN 261 | 262 | # Set the overburden 263 | 264 | _overburden = overburden 265 | 266 | 267 | def get_overburden(): 268 | """Return the set overburden type.""" 269 | 270 | return _overburden 271 | 272 | 273 | # Vertical depth in [km.w.e.] 274 | 275 | 276 | def set_vertical_depth(vertical_depth): 277 | """Set the vertical depth, h. The default is 0.5 km.w.e.""" 278 | 279 | if get_overburden() == "mountain": 280 | 281 | raise Exception( 282 | 'The vertical depth has not been changed because the overburden type is currently set to "mountain". Please either use the constants.load_mountain() function to change the mountain profile or change the overburden type to "flat" with constants.set_overburden().' 283 | ) 284 | 285 | return 286 | 287 | global _vertical_depth 288 | global slant_depths 289 | global angles 290 | 291 | _vertical_depth = vertical_depth 292 | 293 | # Use the set vertical depth to calculate new slant depths and zenith angles 294 | # Only do this for flat overburdens 295 | # For mountains, the slant depths and angles will be calculated in load_mountain() 296 | 297 | if vertical_depth < _X_MIN and not shallow_extrapolation: 298 | 299 | raise Exception( 300 | "The minimum default available slant depth is 0.5 km.w.e. Set constants.shallow_extrapolation to True to enable calculations for depths lower than 0.5 km.w.e. (not recommended)." 301 | ) 302 | 303 | if get_overburden() == "flat": 304 | 305 | slant_depths = np.sort( 306 | np.concatenate( 307 | ([_vertical_depth], _SLANT_DEPTHS[_SLANT_DEPTHS > _vertical_depth]) 308 | ) 309 | ) 310 | angles = np.degrees(np.arccos(_vertical_depth / slant_depths)) 311 | 312 | 313 | def get_vertical_depth(): 314 | """Return the set vertical depth.""" 315 | 316 | if get_overburden() == "mountain": 317 | 318 | print('The overburden type is set to "mountain".') 319 | 320 | return 321 | 322 | return _vertical_depth 323 | 324 | 325 | # Medium 326 | 327 | 328 | def set_medium(medium): 329 | """Set the medium for the muons to be propagated through. The default is standard rock (\"rock\").""" 330 | 331 | assert medium in _accepted_media, f"medium must be set to one of {_accepted_media}." 332 | 333 | global _medium 334 | 335 | _medium = medium 336 | 337 | 338 | def get_medium(): 339 | """Return the set propagation medium.""" 340 | 341 | return _medium 342 | 343 | 344 | # Reference density in [gcm^-3] 345 | 346 | 347 | def set_density(density): 348 | 349 | warnings.warn( 350 | "set_density() is deprecated. This function will be removed in v3.1.0. Please use set_reference_density().", 351 | DeprecationWarning, 352 | stacklevel=2, 353 | ) 354 | 355 | set_reference_density(density) 356 | 357 | 358 | def get_density(): 359 | 360 | warnings.warn( 361 | "get_density() is deprecated. This function will be removed in v3.1.0. Please use get_reference_density().", 362 | DeprecationWarning, 363 | stacklevel=2, 364 | ) 365 | 366 | return get_reference_density() 367 | 368 | 369 | def set_reference_density(reference_density): 370 | """Set the reference density of the propagation medium. The default is 2.65 gcm^-3 (the density of standard rock).""" 371 | 372 | assert reference_density > 0, "Media density must be positive." 373 | 374 | global _reference_density 375 | 376 | if reference_density != 2.65 and get_verbose() >= 1: 377 | 378 | warnings.warn( 379 | "Changing the reference density will trigger the computation of new transfer tensors (for advanced users).", 380 | stacklevel=2, 381 | ) 382 | 383 | _reference_density = reference_density 384 | 385 | 386 | def get_reference_density(): 387 | """Return the set reference density of the propagation medium.""" 388 | 389 | return _reference_density 390 | 391 | 392 | # Number of muon 393 | 394 | 395 | def set_n_muon(n_muon): 396 | """Set the number of muons to be propagated per surface energy-slant depth bin. The default is 100000, as the provided default survival probability tensors in the data directory are given for 100000 muons.""" 397 | 398 | assert isinstance(n_muon, int), "Number of muons must be an integer." 399 | assert n_muon > 0, "Number of muons must be positive." 400 | 401 | global _n_muon 402 | 403 | _n_muon = n_muon 404 | 405 | 406 | def get_n_muon(): 407 | """Return the set number of muons to be propagated per surface energy-slant depth bin.""" 408 | 409 | return _n_muon 410 | 411 | 412 | # Check if required directories exist 413 | 414 | 415 | def _check_directory(directory, force=False): 416 | """This function checks whether a directory required to store output files exists or not. If it does not, ask the user if it should be created.""" 417 | 418 | if not os.path.exists(directory): 419 | 420 | # If the directory does not exist, ask the user if they want to create it 421 | 422 | if not force: 423 | 424 | answer = input( 425 | f"{directory} does not currently exist. Would you like to create it (y/n)?: " 426 | ) 427 | 428 | if force or answer.lower() == "y": 429 | 430 | os.makedirs(directory) 431 | 432 | else: 433 | 434 | print("Directory not created.") 435 | 436 | 437 | # Check that the constants are set to correctly before running any calculation functions 438 | 439 | 440 | def _check_constants(force=False): 441 | """This function checks that the constants are set correctly before running any calculation functions.""" 442 | 443 | # Check that the working directory the user has set exists 444 | 445 | _check_directory(get_directory(), force=force) 446 | 447 | # Check that the overburden has been set to one of the available options 448 | 449 | if _overburden is None: 450 | 451 | raise ValueError('Overburden must be set to either "flat" or "mountain".') 452 | 453 | # Check that the vertical depth is set correctly, if needed 454 | 455 | elif get_overburden() == "flat": 456 | 457 | assert get_vertical_depth() is not None, "Initial vertical depth must be set." 458 | assert get_vertical_depth() > 0, "Initial vertical depth must be positive." 459 | 460 | assert len(slant_depths) == len(angles) and np.allclose( 461 | slant_depths, get_vertical_depth() / np.cos(np.radians(angles)) 462 | ), "slant_depths must correspond to angles. Do not assign constants.slant_depths or constants.angles directly. Instead, use constants.set_vertical_depth() in combination with the angles and depths parameters in individual functions. Run constants.clear() to reset the values of slant_depths and / or angles." 463 | 464 | # Check that a mountain profile has been loaded, if needed 465 | 466 | elif get_overburden() == "mountain": 467 | 468 | if not _mountain_loaded: 469 | 470 | raise Exception( 471 | "The mountain profile must be loaded first by passing a txt file to constants.load_mountain()." 472 | ) 473 | 474 | else: 475 | 476 | raise NotImplementedError( 477 | 'Overburdens of type {0} are not available. The only options are "flat" and "mountain".'.format( 478 | constants.get_overburden() 479 | ) 480 | ) 481 | 482 | # Check that the number of muons is set correctly 483 | 484 | if _n_muon <= 0: 485 | 486 | raise ValueError("Number of muons must be a positive integer.") 487 | 488 | 489 | # Load mountain data 490 | 491 | 492 | def load_mountain( 493 | file_name, file_units="kmwe", rock_density=None, scale=1, max_slant_depth=14 494 | ): 495 | """ 496 | Load data from a mountain profile file. 497 | 498 | The first column should be the zenith angle in [degrees]; the second column should be the azimuthal angle in [degrees]; the third column should be the slant depth in units of units (see below). This function makes available the variables listed under "Sets" below. 499 | 500 | Parameters 501 | ---------- 502 | file_name : str 503 | The full path and name of the .txt file containing the profile information of the mountain. 504 | 505 | file_units : str, optional (default: "kmwe") 506 | The units of the slant depths in file_name. This must be one of ["m", "km", "mwe", "kmwe"]. 507 | 508 | rock_density : float, optional (default: taken from constants.get_reference_density()) 509 | The density in [g cm^-3] of the rock above the lab if the file slant depth units are in [m] or [km]. 510 | 511 | scale : float, optional (default: 1) 512 | The amount by which to scale the slant depths. This is useful if the slant depths in the file were calculated using a density other than the desired density. It can also be used to vary the slant depths within some uncertainty range. 513 | 514 | max_slant_depth : float, optional (default: 14) 515 | The maximum slant depth in [km.w.e.] to take from the file. Any data for slant depths above this value will be set to 0 km.w.e. and will be ignored throughout calculations. The default is 14 km.w.e., consistent with the maximum slant depth in constants._SLANT_DEPTHS being 14 km.w.e. 516 | 517 | Sets 518 | ---- 519 | constants.mountain.zenith : NumPy ndarray 520 | Unique zenith angles in [degrees] sorted from smallest to largest. 521 | 522 | constants.mountain.azimuthal : NumPy ndarray 523 | Unique azimuthal angles in [degrees] sorted from smallest to largest. 524 | 525 | constants.mountain.slant_depths : NumPy ndarray 526 | Unique slant depths in [km.w.e.] in a matrix of shape (len(constants.mountain_zenith), len(constants.mountain_azimuthal)). 527 | """ 528 | 529 | # Check values 530 | 531 | assert ( 532 | get_overburden() == "mountain" 533 | ), 'The overburden type must be set to "mountain".' 534 | assert isinstance(file_name, str), "file_name must be a string." 535 | assert file_units in [ 536 | "m", 537 | "km", 538 | "mwe", 539 | "kmwe", 540 | ], 'Units must be "m", "km", "mwe", or "kmwe".' 541 | 542 | if (file_units == "m" or file_units == "km") and rock_density is None: 543 | 544 | raise ValueError( 545 | "rock_density must be specified when units are {0}.".format(file_units) 546 | ) 547 | 548 | elif (file_units == "mwe" or file_units == "kmwe") and rock_density is not None: 549 | 550 | raise ValueError( 551 | 'rock_density should only be set if file_units is "m" or "km".' 552 | ) 553 | 554 | # Check for use of provided profiles 555 | 556 | provided_profiles = { 557 | "y2l": "", 558 | "superk": 'https://inspirehep.net/literature/824640, https://inspirehep.net/literature/607144, and "Digital Map 50 m Grid (Elevation), Geographical Survey Institute of Japan (1997)."', 559 | "kamland": 'https://inspirehep.net/literature/824640 and "Digital Map 50 m Grid (Elevation), Geographical Survey Institute of Japan (1997)."', 560 | "lngs": "https://inspirehep.net/literature/471316.", 561 | "lsm": "https://inspirehep.net/literature/26080.", 562 | "cjpl": "https://inspirehep.net/literature/1809695.", 563 | } # Wait for email 564 | 565 | file_name_path = file_name 566 | 567 | if file_name.lower() in provided_profiles.keys(): 568 | 569 | file_name_path = os.path.join( 570 | get_directory(), "mountains", file_name.lower() + "_mountain.txt" 571 | ) 572 | 573 | if get_verbose() >= 1 and file_name.lower() != "y2l": 574 | print( 575 | "For use of the {0} mountain profile, please cite {1}".format( 576 | file_name, provided_profiles[file_name.lower()] 577 | ) 578 | ) 579 | 580 | # Global variables 581 | 582 | global _mountain_loaded 583 | global _mountain_zenith_all 584 | global _mountain_azimuthal_all 585 | global _mountain_slant_depths_all 586 | 587 | global mountain 588 | 589 | # Create a named tuple to hold immutable results 590 | 591 | Mountain = namedtuple("Mountain", ("zenith", "azimuthal", "slant_depths")) 592 | 593 | # If the file slant depths are in [m] or [km], convert to [m.w.e.] or [km.w.e.] 594 | # Also convert between rock densities if necessary 595 | 596 | density_mult = 1 597 | 598 | if file_units == "m" or file_units == "km": 599 | 600 | if get_verbose() > 1: 601 | print( 602 | "Multiplying slant depths by {0} gcm^-3 to convert to water-equivalent units.".format( 603 | rock_density, file_units 604 | ) 605 | ) 606 | 607 | density_mult = rock_density 608 | 609 | # If the file slant depths are in [m] or [m.w.e.], convert to [km.w.e.] 610 | 611 | km_scale = 1 612 | 613 | if file_units == "m" or file_units == "mwe": 614 | 615 | if get_verbose() > 1: 616 | print(f"Converting slant depths from [{file_units}] to [km.w.e.].") 617 | 618 | km_scale = 1e-3 619 | 620 | # Scale the slant depths by user-defined scale 621 | 622 | if scale != 1 and get_verbose() > 1: 623 | print(f"Scaling slant depths by a factor of {scale}.") 624 | 625 | # Load the mountain profile data from the file 626 | 627 | _mountain_zenith_all = np.loadtxt(file_name_path)[:, 0] 628 | _mountain_azimuthal_all = np.loadtxt(file_name_path)[:, 1] 629 | _mountain_slant_depths_all = ( 630 | np.loadtxt(file_name_path)[:, 2] * density_mult * km_scale * scale 631 | ) 632 | 633 | # Extract the unique angles and slant depths 634 | 635 | mountain_zenith = np.unique(_mountain_zenith_all) 636 | mountain_azimuthal = np.unique(_mountain_azimuthal_all) 637 | mountain_slant_depths = np.reshape( 638 | np.nan_to_num(_mountain_slant_depths_all), 639 | (len(mountain_zenith), len(mountain_azimuthal)), 640 | ) 641 | 642 | # Remove the slant depths above the maximum slant depth threshold by setting them to 0 643 | 644 | mountain_slant_depths[mountain_slant_depths > max_slant_depth] = 0 645 | 646 | # Assign the elements of the namedtuple 647 | 648 | mountain = Mountain(mountain_zenith, mountain_azimuthal, mountain_slant_depths) 649 | 650 | # If the mountain profile loads successfully, set _mountain_loaded to True 651 | 652 | _mountain_loaded = True 653 | 654 | 655 | # Clear all variables 656 | 657 | 658 | def clear(): 659 | """Reset all of the values set or calculated in MUTE to their default values.""" 660 | 661 | # Import packages 662 | 663 | import gc 664 | 665 | # Energies, slant depths, and zenith angles 666 | 667 | global _E_BINS 668 | global _E_WIDTHS 669 | global ENERGIES 670 | global _X_MIN 671 | global _X_MAX 672 | global _SLANT_DEPTHS 673 | global _ANGLES 674 | global slant_depths 675 | global angles 676 | global ANGLES_FOR_S_FLUXES 677 | 678 | _E_BINS = np.logspace(1.9, 14, 122)[:-30] # [MeV] 679 | _E_WIDTHS = np.diff(_E_BINS) # [MeV] 680 | ENERGIES = np.sqrt(_E_BINS[1:] * _E_BINS[:-1]) # [MeV] 681 | _X_MIN = 0.5 # [km.w.e.] 682 | _X_MAX = 14 # [km.w.e.] 683 | _SLANT_DEPTHS = np.linspace( 684 | _X_MIN, _X_MAX, int(2 * (_X_MAX - _X_MIN) + 1) 685 | ) # [km.w.e.] 686 | _ANGLES = np.degrees(np.arccos(0.5 / _SLANT_DEPTHS)) # [degrees] 687 | slant_depths = _SLANT_DEPTHS # [km.w.e.] 688 | angles = _ANGLES # [degrees] 689 | ANGLES_FOR_S_FLUXES = np.linspace(0, 90, 20) # [degrees] 690 | 691 | # Other constants and variables 692 | 693 | global _MU_MASS 694 | global MONTHS 695 | global MONTHS_SNAMES 696 | global _accepted_media 697 | global _survival_probability_tensor_configuration 698 | global _current_survival_probability_tensor 699 | global _mountain_loaded 700 | global shallow_extrapolation 701 | 702 | _MU_MASS = 105.6583745 # [MeV] 703 | MONTHS = [ 704 | "January", 705 | "February", 706 | "March", 707 | "April", 708 | "May", 709 | "June", 710 | "July", 711 | "August", 712 | "September", 713 | "October", 714 | "November", 715 | "December", 716 | ] 717 | MONTHS_SNAMES = [ 718 | "Jan.", 719 | "Feb.", 720 | "Mar.", 721 | "Apr.", 722 | "May.", 723 | "Jun.", 724 | "Jul.", 725 | "Aug.", 726 | "Sep.", 727 | "Oct.", 728 | "Nov.", 729 | "Dec.", 730 | ] 731 | _accepted_media = [ 732 | "rock", 733 | "frejus_rock", 734 | "y2l_rock", 735 | "salt", 736 | "water", 737 | "antares_water", 738 | "ice", 739 | ] 740 | _survival_probability_tensor_configuration = {} 741 | _current_survival_probability_tensor = None 742 | _mountain_loaded = False 743 | shallow_extrapolation = False 744 | 745 | # Global constants and variables 746 | 747 | global _verbose 748 | global _output 749 | global _directory 750 | global _lab 751 | global _overburden 752 | global _vertical_depth 753 | global _medium 754 | global _reference_density 755 | global _n_muon 756 | 757 | _verbose = 2 758 | _output = True 759 | _directory = os.path.join(os.path.dirname(__file__), "data") 760 | _lab = "Default" 761 | _overburden = "flat" 762 | _vertical_depth = _X_MIN 763 | _medium = "rock" 764 | _reference_density = 2.65 765 | _n_muon = 1000000 766 | 767 | # Global mountain variables 768 | # Set the variables to None first, in case they do not already exist in the namespace 769 | 770 | global _mountain_zenith_all 771 | global _mountain_azimuthal_all 772 | global _mountain_slant_depths_all 773 | global mountain 774 | 775 | _mountain_zenith_all = None 776 | _mountain_azimuthal_all = None 777 | _mountain_slant_depths_all = None 778 | mountain = None 779 | 780 | del _mountain_zenith_all 781 | del _mountain_azimuthal_all 782 | del _mountain_slant_depths_all 783 | del mountain 784 | 785 | gc.collect() 786 | 787 | if get_verbose() > 1: 788 | print("Reset global constants to their default values.") 789 | -------------------------------------------------------------------------------- /src/mute/surface.py: -------------------------------------------------------------------------------- 1 | ######################### 2 | ######################### 3 | ### ### 4 | ### MUTE ### 5 | ### William Woodley ### 6 | ### 24 May 2025 ### 7 | ### ### 8 | ######################### 9 | ######################### 10 | 11 | # Import packages 12 | 13 | import os 14 | 15 | import numpy as np 16 | import scipy.integrate as scii 17 | from tqdm import tqdm 18 | import warnings 19 | 20 | import mute.constants as constants 21 | 22 | # Calculate surface fluxes 23 | 24 | 25 | def calc_s_fluxes( 26 | model="mceq", 27 | output=None, 28 | file_name="", 29 | force=False, 30 | test=False, 31 | angles=constants.ANGLES_FOR_S_FLUXES, 32 | **kwargs, 33 | ): 34 | """ 35 | Calculate surface fluxes in units of [(cm^2 s sr MeV)^-1] for default surface energy grid and surface flux zenith angles. 36 | 37 | The default surface energy grid is given by constants.ENERGIES, and the default zenith angles are given by constants.ANGLES_FOR_S_FLUXES. 38 | 39 | Parameters 40 | ---------- 41 | model : str in {"daemonflux", "mceq"}, optional (default: "mceq") 42 | The model to use to calculate surface fluxes. MCeq provides the option to specify primary, interaction, and density model keyword arguments (see Other Parameters), while daemonflux uses "gsf" as the primary model and "ddm" as the interaction model. The default model will be changed to "daemonflux" in v3.1.0. This parameter is case-insensitive. 43 | 44 | output : bool, optional (default: taken from constants.get_output()) 45 | If True, an output file will be created to store the results. 46 | 47 | file_name : str, optional (default: constructed from input parameters) 48 | The name of the file in which to store the results. If output is False or None, this is ignored. 49 | 50 | force : bool, optional (default: False) 51 | If True, force the creation of new directories if required. 52 | 53 | test: bool, optional (default: False) 54 | For use in the file test_s_fluxes.py. If True, this will calculate surface fluxes for only three angles. 55 | 56 | angles : array-like, optional (default: taken from consants.ANGLES_FOR_S_FLUXES) 57 | An array of zenith angles in [degrees] to calculate the surface fluxes for. 58 | 59 | Other Parameters 60 | ----------------- 61 | return_error : bool, optional (default: False) 62 | If True, if model == "daemonflux", return the error on the surface fluxes instead of the surface fluxes. 63 | 64 | primary_model : str in {"gsf", "hg", "h3a", "h4a", "gh", "gst3", "gst4", "zs", "zsp", "pl27"} or tuple, optional (default: "gsf") 65 | The primary flux model to use in MCEq. This parameter is case-insensitive. Options: 66 | gsf = GlobalSplineFitBeta 67 | hg = HillasGaisser2012 (H3a) 68 | h3a = HillasGaisser2012 (H3a) 69 | h4a = HillasGaisser2012 (H4a) 70 | gh = GaisserHonda 71 | gst3 = GaisserStanevTilav (3-gen) 72 | gst4 = GaisserStanevTilav (4-gen) 73 | zs = Zatsepin-Sokolskaya (Default) 74 | zsp = Zatsepin-Sokolskaya (PAMELA) 75 | pl27 = SimplePowerlaw27 76 | Alternatively, this can be set with a tuple. For example: (pm.GaisserStanevTilav, "3-gen"). 77 | 78 | interaction_model : str, optional (default: "sibyll23c") 79 | The hadronic interaction model to use in MCEq. See the Tutorial or MCEq documentation for a list of options. This parameter is case-insensitive. 80 | 81 | atmosphere : {"corsika", "msis00"}, optional (default: "corsika") 82 | The atmospheric model to use in MCEq. For US Standard Atmosphere, use "corsika". For seasonal variations, use "msis00". This parameter is case-insensitive. 83 | 84 | location : str or tuple, optional (default: "USStd") 85 | The name of the location for which to calculate the surface fluxes. See the Tutorial or MCEq documentation for a list of options. Alternatively, this can be set with a tuple of shape (latitude, longitude). This parameter is case-sensitive. 86 | 87 | month : str, optional (default: None) 88 | The month for which to calculate the surface fluxes. For US Standard Atmosphere, use None. For seasonal variations, use the month name. This parameter is case-sensitive. 89 | 90 | Returns 91 | ------- 92 | s_fluxes : NumPy ndarray 93 | A two-dimensional array containing the surface fluxes (or the errors thereof). The default shape will be (91, 20), and the fluxes will be in units of [(cm^2 s sr MeV)^-1]. 94 | """ 95 | 96 | # Check values 97 | 98 | constants._check_constants(force=force) 99 | 100 | if output is None: 101 | output = constants.get_output() 102 | 103 | model = model.lower() 104 | 105 | assert model in [ 106 | "daemonflux", 107 | "mceq", 108 | ], "The model must either be 'daemonflux' or 'mceq'." 109 | 110 | # Set the angles 111 | 112 | test_file_name = "" 113 | 114 | if test: 115 | 116 | angles = [0, 30, 60] 117 | test_file_name = "_pytest" 118 | 119 | # Calculate surface fluxes with DAEMONFLUX 120 | 121 | if model == "daemonflux": 122 | 123 | # Import the Flux class 124 | 125 | import daemonflux 126 | 127 | # Pop the keyword arguments 128 | # This keyword must be popped instead of gotten, since it should not be passed into the DAEMONFLUX Flux() function 129 | 130 | return_error = kwargs.pop("return_error", False) 131 | 132 | # Calculate surface fluxes and errors 133 | 134 | df_flux = daemonflux.Flux(**kwargs) 135 | 136 | e_grid = 1e-3 * constants.ENERGIES 137 | e_grid_div = ( 138 | np.reshape(np.repeat(e_grid, len(angles)), (len(e_grid), len(angles))) 139 | ) ** 3 140 | 141 | if constants.get_verbose() > 1: 142 | print("Calculating surface fluxes.") 143 | 144 | # Calculate surface fluxes or surface flux errors 145 | 146 | if not return_error: 147 | 148 | s_fluxes = 1e-3 * ( 149 | df_flux.flux(e_grid, angles, "total_muflux") / e_grid_div 150 | ) 151 | 152 | else: 153 | 154 | s_fluxes = 1e-3 * ( 155 | df_flux.error(e_grid, angles, "total_muflux") / e_grid_div 156 | ) 157 | 158 | if constants.get_verbose() > 1: 159 | print("Finished calculating surface fluxes.") 160 | 161 | # Calculate surface fluxes with MCEq 162 | 163 | elif model == "mceq": 164 | 165 | # Import packages 166 | 167 | import MCEq 168 | from MCEq.core import MCEqRun 169 | import crflux.models as pm 170 | 171 | # Get the keyword arguments 172 | 173 | primary_model = kwargs.get("primary_model", "gsf") 174 | interaction_model = ( 175 | kwargs.get("interaction_model", "sibyll23c") 176 | .replace("-", "") 177 | .replace(".", "") 178 | .lower() 179 | ) 180 | atmosphere = kwargs.get("atmosphere", "corsika").lower() 181 | location = kwargs.get("location", "USStd") 182 | month = kwargs.get("month", None) 183 | 184 | # Check values 185 | 186 | return_error = kwargs.pop("return_error", None) 187 | 188 | if return_error is not None: 189 | raise ValueError( 190 | 'Errors cannot be calculated with MCEq. To include errors in your calculation, please use daemonflux by setting model = "daemonflux".' 191 | ) 192 | 193 | assert atmosphere in [ 194 | "corsika", 195 | "msis00", 196 | ], 'atmosphere must be set to either "corsika" or "msis00".' 197 | 198 | primary_models = { 199 | "gsf": (pm.GlobalSplineFitBeta, None), 200 | "hg": (pm.HillasGaisser2012, "H3a"), 201 | "h3a": (pm.HillasGaisser2012, "H3a"), 202 | "h4a": (pm.HillasGaisser2012, "H4a"), 203 | "gh": (pm.GaisserHonda, None), 204 | "gst3": (pm.GaisserStanevTilav, "3-gen"), 205 | "gst4": (pm.GaisserStanevTilav, "4-gen"), 206 | "zs": (pm.ZatsepinSokolskaya, "default"), 207 | "zsp": (pm.ZatsepinSokolskaya, "pamela"), 208 | "pl27": (pm.SimplePowerlaw27, None), 209 | } 210 | 211 | if isinstance(primary_model, str): 212 | 213 | assert ( 214 | primary_model.lower() in primary_models 215 | ), "Set primary model not available. See the available options in the Tutorial at {0}.".format( 216 | "https://github.com/wjwoodley/mute/blob/main/docs/Tutorial_Models.md#primary-model" 217 | ) 218 | 219 | if primary_model == "hg": 220 | 221 | if constants.get_verbose() >= 1: 222 | 223 | warnings.warn( 224 | 'The "hg" option is deprecated. This option will be removed in v3.1.0. Please use "h3a" or "h4a".', 225 | DeprecationWarning, 226 | stacklevel=2, 227 | ) 228 | 229 | primary_model_for_MCEq = primary_models[primary_model.lower()] 230 | pm_sname = primary_model.lower() 231 | 232 | elif isinstance(primary_model, tuple) and len(primary_model) == 2: 233 | 234 | primary_model_for_MCEq = primary_model 235 | pm_sname = primary_model[0](primary_model[1]).sname.lower() 236 | 237 | else: 238 | 239 | raise TypeError( 240 | "Primary model not set correctly. For an explanation, see the Tutorial at {0}.".format( 241 | "https://github.com/wjwoodley/mute/blob/main/docs/Tutorial_Models.md#primary-model" 242 | ) 243 | ) 244 | 245 | # Check that the location is set correctly 246 | 247 | location_is_str = isinstance(location, str) 248 | location_is_tuple = isinstance(location, tuple) and len(location) == 2 249 | 250 | if not (location_is_str or location_is_tuple): 251 | 252 | raise TypeError( 253 | "Location not set correctly. For an explanation, see the Tutorial at {0}.".format( 254 | "https://github.com/wjwoodley/mute/blob/main/docs/Tutorial_Models.md#atmospheric-model" 255 | ) 256 | ) 257 | 258 | if (location_is_str and location != "USStd") or location_is_tuple: 259 | 260 | assert ( 261 | interaction_model != "ddm" and interaction_model != "sibyll23d" 262 | ), "{0} is not currently supported for varying atmospheres in MCEq. Please use a different interaction model or US Standard Atmosphere.".format( 263 | interaction_model 264 | ) 265 | assert ( 266 | atmosphere == "msis00" 267 | ), 'atmosphere must be set to "msis00" if location is specified.' 268 | assert ( 269 | month is not None 270 | ), "month must be specified if location is specified." 271 | 272 | # Set MCEq up 273 | 274 | MCEq.config.enable_default_tracking = False 275 | 276 | if constants.get_verbose() > 1: 277 | 278 | print( 279 | "Calculating surface fluxes for {0} using {1} and {2}.".format( 280 | location, pm_sname, interaction_model 281 | ) 282 | ) 283 | MCEq.config.debug_level = 1 284 | 285 | else: 286 | 287 | MCEq.config.debug_level = 0 288 | 289 | if isinstance(location, str): 290 | 291 | mceq_run = MCEqRun( 292 | interaction_model=interaction_model, 293 | primary_model=primary_model_for_MCEq, 294 | theta_deg=0.0, 295 | ) 296 | 297 | mceq_run.set_density_model((atmosphere.upper(), (location, month))) 298 | 299 | elif isinstance(location, tuple): 300 | 301 | MCEq.config.density_model = ("MSIS00", ("SouthPole", "January")) 302 | 303 | mceq_run = MCEqRun( 304 | interaction_model=interaction_model, 305 | primary_model=primary_model_for_MCEq, 306 | theta_deg=0.0, 307 | ) 308 | 309 | density_model_MSIS00 = mceq_run.density_model 310 | density_model_MSIS00.set_location_coord(*location[::-1]) 311 | density_model_MSIS00.set_season(month) 312 | 313 | # Calculate the surface fluxes 314 | # Zeroth index = Surface energy 315 | # First index = Zenith angle 316 | 317 | s_fluxes = np.zeros((len(constants.ENERGIES), len(angles))) 318 | 319 | # Run MCEq 320 | # Convert the surface fluxes from default [GeV] to [MeV] 321 | 322 | for j in ( 323 | tqdm(range(len(angles))) 324 | if constants.get_verbose() >= 1 325 | else range(len(angles)) 326 | ): 327 | 328 | # Solve for the given zenith angle 329 | 330 | mceq_run.set_theta_deg(angles[j]) 331 | mceq_run.solve() 332 | 333 | # Store the results in the s_fluxes matrix 334 | 335 | s_fluxes[:, j] = ( 336 | 1e-3 337 | * ( 338 | mceq_run.get_solution("total_mu+", mag=0) 339 | + mceq_run.get_solution("total_mu-", mag=0) 340 | )[:-30] 341 | ) 342 | 343 | if constants.get_verbose() > 1: 344 | print("Finished calculating surface fluxes.") 345 | 346 | else: 347 | 348 | raise NotImplementedError( 349 | 'Model of type {0} is not available. The only options are "daemonflux" and "mceq".'.format( 350 | model 351 | ) 352 | ) 353 | 354 | # Write the results to the file 355 | 356 | if output: 357 | 358 | constants._check_directory( 359 | os.path.join(constants.get_directory(), "surface"), force=force 360 | ) 361 | 362 | if file_name == "" or not isinstance(file_name, str): 363 | 364 | if model == "daemonflux": 365 | 366 | error_file = "" if not return_error else "_error" 367 | file_name = os.path.join( 368 | constants.get_directory(), 369 | "surface", 370 | "surface_fluxes_daemonflux{0}{1}.txt".format( 371 | error_file, test_file_name 372 | ), 373 | ) 374 | 375 | elif model == "mceq": 376 | 377 | file_name = os.path.join( 378 | constants.get_directory(), 379 | "surface", 380 | "surface_fluxes_{0}_{1}_{2}_{3}{4}.txt".format( 381 | location, 382 | str(month), 383 | interaction_model, 384 | pm_sname, 385 | test_file_name, 386 | ), 387 | ) 388 | 389 | else: 390 | 391 | raise NotImplementedError( 392 | 'Model of type {0} is not available. The only options are "daemonflux" and "mceq".'.format( 393 | model 394 | ) 395 | ) 396 | 397 | file_out = open(file_name, "w") 398 | 399 | for i in range(len(constants.ENERGIES)): 400 | 401 | for j in range(len(angles)): 402 | 403 | file_out.write( 404 | "{0:1.14f} {1:1.5f} {2:1.14e}\n".format( 405 | constants.ENERGIES[i], angles[j], s_fluxes[i, j] 406 | ) 407 | ) 408 | 409 | file_out.close() 410 | 411 | if constants.get_verbose() > 1: 412 | print(f"Surface fluxes written to {file_name}.") 413 | 414 | return s_fluxes 415 | 416 | 417 | # Get surface fluxes 418 | 419 | 420 | def load_s_fluxes_from_file( 421 | model="mceq", file_name="", force=False, test=False, **kwargs 422 | ): 423 | """ 424 | Retrieve a surface fluxes matrix in units of [(cm^2 s sr MeV)^-1] stored in data/surface based on the input parameters. 425 | 426 | If file_name is not given, this function searches for a file name that matches the set location, month, interaction model, and primary model. If the file does not exist, prompt the user to run calc_s_fluxes(). 427 | 428 | Parameters 429 | ---------- 430 | model : str in {"daemonflux", "mceq"}, optional (default: "mceq") 431 | The model to use to calculate surface fluxes. MCeq provides the option to specify primary, interaction, and density model keyword arguments (see Other Parameters), while daemonflux uses "gsf" as the primary model and "DDM" as the interaction model. The default model will be changed to "daemonflux" in v3.1.0. This parameter is case-insensitive. 432 | 433 | file_name : str, optional (default: constructed from input parameters) 434 | The name of the file from which to load the surface fluxes. This must be the full path to the file. 435 | 436 | force : bool, optional (default: False) 437 | If True, force the calculation of a new surface flux matrix and the creation of new directories if required. 438 | 439 | test: bool, optional (default: False) 440 | For use in the file test_s_fluxes.py. If True, this will calculate surface fluxes for only three angles. 441 | 442 | Other Parameters 443 | ----------------- 444 | return_error : bool, optional (default: False) 445 | If True, if model == "daemonflux", return the error on the surface fluxes instead of the surface fluxes. 446 | 447 | primary_model : str in {"gsf", "hg", "h3a", "h4a", "gh", "gst3", "gst4", "zs", "zsp", "pl27"} or tuple, optional (default: "gsf") 448 | The primary flux model to use in MCEq. This parameter is case-insensitive. Options: 449 | gsf = GlobalSplineFitBeta 450 | hg = HillasGaisser2012 (H3a) 451 | h3a = HillasGaisser2012 (H3a) 452 | h4a = HillasGaisser2012 (H4a) 453 | gh = GaisserHonda 454 | gst3 = GaisserStanevTilav (3-gen) 455 | gst4 = GaisserStanevTilav (4-gen) 456 | zs = Zatsepin-Sokolskaya (Default) 457 | zsp = Zatsepin-Sokolskaya (PAMELA) 458 | pl27 = SimplePowerlaw27 459 | Alternatively, this can be set with a tuple. For example: (pm.GaisserStanevTilav, "3-gen"). 460 | 461 | interaction_model : str, optional (default: "sibyll23c") 462 | The hadronic interaction model to use in MCEq. See the Tutorial or MCEq documentation for a list of options. This parameter is case-insensitive. 463 | 464 | atmosphere : {"corsika", "msis00"}, optional (default: "corsika") 465 | The atmospheric model to use in MCEq. For US Standard Atmosphere, use "corsika". For seasonal variations, use "msis00". This parameter is case-insensitive. 466 | 467 | location : str or tuple, optional (default: "USStd") 468 | The name of the location for which to calculate the surface fluxes. See the Tutorial or MCEq documentation for a list of options. Alternatively, this can be set with a tuple of shape (latitude, longitude). This parameter is case-sensitive. 469 | 470 | month : str, optional (default: None) 471 | The month for which to calculate the surface fluxes. For US Standard Atmosphere, use None. For seasonal variations, use the month name. This parameter is case-sensitive. 472 | 473 | Returns 474 | ------- 475 | s_fluxes : NumPy ndarray 476 | A two-dimensional array containing the surface fluxes. The shape will be (91, 20), and the fluxes will be in units of [(cm^2 s sr MeV)^-1]. 477 | """ 478 | 479 | # Check values 480 | 481 | constants._check_constants(force=force) 482 | 483 | model = model.lower() 484 | 485 | if model == "daemonflux": 486 | 487 | # Get the keyword arguments 488 | 489 | return_error = kwargs.get("return_error", False) 490 | 491 | elif model == "mceq": 492 | 493 | # Get the keyword arguments 494 | 495 | primary_model = kwargs.get("primary_model", "gsf") 496 | interaction_model = ( 497 | kwargs.get("interaction_model", "sibyll23c") 498 | .replace("-", "") 499 | .replace(".", "") 500 | .lower() 501 | ) 502 | atmosphere = kwargs.get("atmosphere", "corsika").lower() 503 | location = kwargs.get("location", "USStd") 504 | month = kwargs.get("month", None) 505 | 506 | # Check values 507 | 508 | if isinstance(primary_model, str): 509 | 510 | pass 511 | 512 | elif isinstance(primary_model, tuple): 513 | 514 | raise TypeError( 515 | "The primary model must be set with a string. To set it with a tuple, run the surface.calc_s_fluxes() function with output set to True. For an explanation, see the Tutorial at {0}.".format( 516 | "https://github.com/wjwoodley/mute/blob/main/docs/Tutorial_Models.md#primary-model" 517 | ) 518 | ) 519 | 520 | else: 521 | 522 | raise TypeError( 523 | "Primary model not set correctly. For an explanation, see the Tutorial at {0}.".format( 524 | "https://github.com/wjwoodley/mute/blob/main/docs/Tutorial_Models.md#primary-model" 525 | ) 526 | ) 527 | 528 | else: 529 | 530 | raise NotImplementedError( 531 | 'Model of type {0} is not available. The only options are "daemonflux" and "mceq".'.format( 532 | model 533 | ) 534 | ) 535 | 536 | # Set the angles 537 | 538 | angles = constants.ANGLES_FOR_S_FLUXES 539 | 540 | test_file_name = "" 541 | 542 | if test: 543 | angles = [0, 30, 60] 544 | test_file_name = "_pytest" 545 | 546 | # Define a function to run if there is no surface fluxes file 547 | 548 | def no_file(force): 549 | 550 | # If the file does not exist, ask the user if they want to run MCEq to create it 551 | 552 | if not force: 553 | answer = input( 554 | "No surface flux matrix currently exists for these models. Would you like to create one (y/n)?: " 555 | ) 556 | 557 | if force or answer.lower() == "y": 558 | 559 | s_fluxes = calc_s_fluxes(model=model, force=force, test=test, **kwargs) 560 | 561 | return s_fluxes 562 | 563 | else: 564 | 565 | print("Surface fluxes not calculated.") 566 | 567 | return None 568 | 569 | # Construct a file name based on the user's inputs if one has not been provided 570 | 571 | if file_name == "" or not isinstance(file_name, str): 572 | 573 | if model == "daemonflux": 574 | 575 | error_file = "" if not return_error else "_error" 576 | file_name = os.path.join( 577 | constants.get_directory(), 578 | "surface", 579 | "surface_fluxes_daemonflux{0}{1}.txt".format( 580 | error_file, test_file_name 581 | ), 582 | ) 583 | 584 | elif model == "mceq": 585 | 586 | file_name = os.path.join( 587 | constants.get_directory(), 588 | "surface", 589 | "surface_fluxes_{0}_{1}_{2}_{3}{4}.txt".format( 590 | location, 591 | str(month), 592 | interaction_model, 593 | primary_model.lower(), 594 | test_file_name, 595 | ), 596 | ) 597 | 598 | # Check if the file exists 599 | 600 | if os.path.exists(file_name): 601 | 602 | if constants.get_verbose() > 1: 603 | 604 | if model == "daemonflux": 605 | 606 | print("Loading surface fluxes for daemonflux.") 607 | 608 | elif model == "mceq": 609 | 610 | print( 611 | "Loading surface fluxes for {0} using {1} and {2}.".format( 612 | location, primary_model.lower(), interaction_model 613 | ) 614 | ) 615 | 616 | # Check that the file has the correct numbers of energies and zenith angles 617 | 618 | file = open(file_name, "r") 619 | n_lines = len(file.read().splitlines()) 620 | file.close() 621 | 622 | # If so, read the surface fluxes in from it 623 | 624 | if n_lines == len(constants.ENERGIES) * len(angles): 625 | 626 | s_fluxes = np.reshape( 627 | np.loadtxt(file_name)[:, 2], (len(constants.ENERGIES), len(angles)) 628 | ) 629 | 630 | if constants.get_verbose() > 1: 631 | print("Loaded surface fluxes.") 632 | 633 | return s_fluxes 634 | 635 | # If the file does not have the correct numbers of energies and zenith angles, run the no_file() function 636 | 637 | else: 638 | 639 | return no_file(force=force) 640 | 641 | # If the file does not exist, run the no_file() function 642 | 643 | else: 644 | 645 | return no_file(force=force) 646 | 647 | 648 | # Calculate the surface intensities 649 | 650 | 651 | def calc_s_intensities( 652 | output=None, 653 | file_name="", 654 | force=False, 655 | **kwargs, 656 | ): 657 | """ 658 | Calculate surface intensities in units of [(cm^2 s sr)^-1] for default surface energy grid and surface flux zenith angles. 659 | 660 | The default surface energy grid is given by constants.ENERGIES, and the default zenith angles are given by constants.ANGLES_FOR_S_FLUXES. 661 | 662 | Parameters 663 | ---------- 664 | output : bool, optional (default: taken from constants.get_output()) 665 | If True, an output file will be created to store the results. 666 | 667 | file_name : str, optional (default: constructed from input parameters) 668 | The name of the file in which to store the results. If output is False or None, this is ignored. 669 | 670 | force : bool, optional (default: False) 671 | If True, force the calculation of a new surface flux matrix and the creation of new directories if required. 672 | 673 | Other Parameters 674 | ---------------- 675 | s_fluxes : NumPy ndarray, optional (default: taken from surface.load_s_fluxes_from_file()) 676 | A surface flux matrix of shape (91, 20). 677 | 678 | model : str in {"daemonflux", "mceq"}, optional (default: "mceq") 679 | The model to use to calculate surface fluxes. MCeq provides the option to specify primary, interaction, and density model keyword arguments, while DAEMONFLUX uses "gsf" as the primary model and "ddm" as the interaction model. The default model will be changed to "daemonflux" in v3.1.0. This parameter is case-insensitive. 680 | 681 | return_error : bool, optional (default: False) 682 | If True, if model == "daemonflux", return the error on the surface fluxes instead of the surface fluxes. 683 | 684 | primary_model : str in {"gsf", "hg", "h3a", "h4a", "gh", "gst3", "gst4", "zs", "zsp", "pl27"} or tuple, optional (default: "gsf") 685 | The primary flux model to use in MCEq. This parameter is case-insensitive. Options: 686 | gsf = GlobalSplineFitBeta 687 | hg = HillasGaisser2012 (H3a) 688 | h3a = HillasGaisser2012 (H3a) 689 | h4a = HillasGaisser2012 (H4a) 690 | gh = GaisserHonda 691 | gst3 = GaisserStanevTilav (3-gen) 692 | gst4 = GaisserStanevTilav (4-gen) 693 | zs = Zatsepin-Sokolskaya (Default) 694 | zsp = Zatsepin-Sokolskaya (PAMELA) 695 | pl27 = SimplePowerlaw27 696 | Alternatively, this can be set with a tuple. For example: (pm.GaisserStanevTilav, "3-gen"). 697 | 698 | interaction_model : str, optional (default: "sibyll23c") 699 | The hadronic interaction model to use in MCEq. See the Tutorial or MCEq documentation for a list of options. This parameter is case-insensitive. 700 | 701 | atmosphere : {"corsika", "msis00"}, optional (default: "corsika") 702 | The atmospheric model to use in MCEq. For US Standard Atmosphere, use "corsika". For seasonal variations, use "msis00". This parameter is case-insensitive. 703 | 704 | location : str or tuple, optional (default: "USStd") 705 | The name of the location for which to calculate the surface fluxes. See the Tutorial or MCEq documentation for a list of options. Alternatively, this can be set with a tuple of shape (latitude, longitude). This parameter is case-sensitive. 706 | 707 | month : str, optional (default: None) 708 | The month for which to calculate the surface fluxes. For US Standard Atmosphere, use None. For seasonal variations, use the month name. This parameter is case-sensitive. 709 | 710 | Returns 711 | ------- 712 | s_intensities : NumPy ndarray 713 | A one-dimensional array containing the surface intensities. The length will be 20, and the intensities will be in units of [(cm^2 s sr)^-1]. 714 | """ 715 | 716 | # Check values 717 | 718 | constants._check_constants() 719 | 720 | if output is None: 721 | output = constants.get_output() 722 | 723 | # Get the keyword arguments 724 | 725 | s_fluxes = kwargs.get("s_fluxes", None) 726 | model = kwargs.get("model", "mceq").lower() 727 | 728 | # Set the model to None if an s_fluxes matrix is provided by the user 729 | # This will prevent model information from being used in the construction of file_name 730 | 731 | if s_fluxes is not None: 732 | model = None 733 | 734 | # Get the keyword arguments 735 | # Construct output file names for use if output is True 736 | 737 | if output: 738 | constants._check_directory( 739 | os.path.join(constants.get_directory(), "surface"), force=force 740 | ) 741 | 742 | if model == "daemonflux": 743 | 744 | return_error = kwargs.get("return_error", False) 745 | 746 | if output: 747 | 748 | if file_name == "" or not isinstance(file_name, str): 749 | 750 | error_file = "" if not return_error else "_error" 751 | file_name = os.path.join( 752 | constants.get_directory(), 753 | "surface", 754 | "surface_intensities_daemonflux{0}.txt".format(error_file), 755 | ) 756 | 757 | elif model == "mceq": 758 | 759 | primary_model = kwargs.get("primary_model", "gsf").lower() 760 | interaction_model = ( 761 | kwargs.get("interaction_model", "sibyll23c") 762 | .replace("-", "") 763 | .replace(".", "") 764 | .lower() 765 | ) 766 | location = kwargs.get("location", "USStd") 767 | month = kwargs.get("month", None) 768 | 769 | if output: 770 | 771 | if file_name == "" or not isinstance(file_name, str): 772 | 773 | file_name = os.path.join( 774 | constants.get_directory(), 775 | "surface", 776 | "surface_intensities_{0}_{1}_{2}_{3}.txt".format( 777 | location, str(month), interaction_model, primary_model 778 | ), 779 | ) 780 | 781 | elif model is None: 782 | 783 | if output: 784 | 785 | if file_name == "" or not isinstance(file_name, str): 786 | 787 | file_name = os.path.join( 788 | constants.get_directory(), "surface", "surface_intensities.txt" 789 | ) 790 | 791 | else: 792 | 793 | NotImplementedError( 794 | 'Model of type {0} is not available. The only options are "daemonflux" and "mceq".'.format( 795 | model 796 | ) 797 | ) 798 | 799 | # Get the surface flux matrix 800 | 801 | if constants.get_verbose() > 1: 802 | print("Calculating surface intensities.") 803 | 804 | if s_fluxes is None: 805 | s_fluxes = load_s_fluxes_from_file(force=force, **kwargs) 806 | 807 | # Check that the surface flux matrix has been loaded properly 808 | 809 | if s_fluxes is None: 810 | raise Exception( 811 | "Surface intensities not calculated. The surface flux matrix was not provided or loaded correctly." 812 | ) 813 | 814 | s_fluxes = np.atleast_2d(s_fluxes) 815 | 816 | # Calculate the surface intensities 817 | 818 | s_intensities = scii.simpson(s_fluxes, x=constants.ENERGIES, axis=0) 819 | 820 | if constants.get_verbose() > 1: 821 | print("Finished calculating surface intensities.") 822 | 823 | # Write the results to the file 824 | 825 | if output: 826 | 827 | file_out = open(file_name, "w") 828 | 829 | for j in range(len(constants.ANGLES_FOR_S_FLUXES)): 830 | 831 | file_out.write( 832 | "{0:1.5f} {1:1.14e}\n".format( 833 | constants.ANGLES_FOR_S_FLUXES[j], s_intensities[j] 834 | ) 835 | ) 836 | 837 | file_out.close() 838 | 839 | if constants.get_verbose() > 1: 840 | print(f"Surface intensities written to {file_name}.") 841 | 842 | return s_intensities 843 | 844 | 845 | # Calculate surface energy spectra 846 | 847 | 848 | def calc_s_e_spect(output=None, file_name="", force=False, **kwargs): 849 | """ 850 | Calculate a surface energy spectrum in units of [(cm^2 s MeV)^-1] for default surface energy grid. 851 | 852 | The default surface energy grid is given by constants.ENERGIES. 853 | 854 | Parameters 855 | ---------- 856 | output : bool, optional (default: taken from constants.get_output()) 857 | If True, an output file will be created to store the results. 858 | 859 | file_name : str, optional (default: constructed from input parameters) 860 | The name of the file in which to store the results. If output is False or None, this is ignored. 861 | 862 | force : bool, optional (default: False) 863 | If True, force the calculation of a new surface flux matrix and the creation of new directories if required. 864 | 865 | Other Parameters 866 | ---------------- 867 | s_fluxes : NumPy ndarray, optional (default: taken from surface.load_s_fluxes_from_file()) 868 | A surface flux matrix of shape (91, 20). 869 | 870 | model : str in {"daemonflux", "mceq"}, optional (default: "mceq") 871 | The model to use to calculate surface fluxes. MCeq provides the option to specify primary, interaction, and density model keyword arguments, while DAEMONFLUX uses "gsf" as the primary model and "ddm" as the interaction model. The default model will be changed to "daemonflux" in v3.1.0. This parameter is case-insensitive. 872 | 873 | return_error : bool, optional (default: False) 874 | If True, if model == "daemonflux", return the error on the surface fluxes instead of the surface fluxes. 875 | 876 | primary_model : str in {"gsf", "hg", "h3a", "h4a", "gh", "gst3", "gst4", "zs", "zsp", "pl27"} or tuple, optional (default: "gsf") 877 | The primary flux model to use in MCEq. This parameter is case-insensitive. Options: 878 | gsf = GlobalSplineFitBeta 879 | hg = HillasGaisser2012 (H3a) 880 | h3a = HillasGaisser2012 (H3a) 881 | h4a = HillasGaisser2012 (H4a) 882 | gh = GaisserHonda 883 | gst3 = GaisserStanevTilav (3-gen) 884 | gst4 = GaisserStanevTilav (4-gen) 885 | zs = Zatsepin-Sokolskaya (Default) 886 | zsp = Zatsepin-Sokolskaya (PAMELA) 887 | pl27 = SimplePowerlaw27 888 | Alternatively, this can be set with a tuple. For example: (pm.GaisserStanevTilav, "3-gen"). 889 | 890 | interaction_model : str, optional (default: "sibyll23c") 891 | The hadronic interaction model to use in MCEq. See the Tutorial or MCEq documentation for a list of options. This parameter is case-insensitive. 892 | 893 | atmosphere : {"corsika", "msis00"}, optional (default: "corsika") 894 | The atmospheric model to use in MCEq. For US Standard Atmosphere, use "corsika". For seasonal variations, use "msis00". This parameter is case-insensitive. 895 | 896 | location : str or tuple, optional (default: "USStd") 897 | The name of the location for which to calculate the surface fluxes. See the Tutorial or MCEq documentation for a list of options. Alternatively, this can be set with a tuple of shape (latitude, longitude). This parameter is case-sensitive. 898 | 899 | month : str, optional (default: None) 900 | The month for which to calculate the surface fluxes. For US Standard Atmosphere, use None. For seasonal variations, use the month name. This parameter is case-sensitive. 901 | 902 | Returns 903 | ------- 904 | s_e_spect : NumPy ndarray 905 | A one-dimensional array containing the surface energy spectrum. The length will be 91, and the energy spectrum values will be in units of [(cm^2 s GeV)^-1]. 906 | """ 907 | 908 | # Check values 909 | 910 | constants._check_constants() 911 | 912 | if output is None: 913 | output = constants.get_output() 914 | 915 | # Get the keyword arguments 916 | 917 | s_fluxes = kwargs.get("s_fluxes", None) 918 | model = kwargs.get("model", "mceq").lower() 919 | 920 | # Set the model to None if an s_fluxes matrix is provided by the user 921 | # This will prevent model information from being used in the construction of file_name 922 | 923 | if s_fluxes is not None: 924 | model = None 925 | 926 | # Get the keyword arguments 927 | # Construct output file names for use if output is True 928 | 929 | if output: 930 | constants._check_directory( 931 | os.path.join(constants.get_directory(), "surface"), force=force 932 | ) 933 | 934 | if model == "daemonflux": 935 | 936 | return_error = kwargs.get("return_error", False) 937 | 938 | if output: 939 | 940 | if file_name == "" or not isinstance(file_name, str): 941 | 942 | error_file = "" if not return_error else "_error" 943 | file_name = os.path.join( 944 | constants.get_directory(), 945 | "surface", 946 | "surface_energy_spectrum_daemonflux{0}.txt".format(error_file), 947 | ) 948 | 949 | elif model == "mceq": 950 | 951 | primary_model = kwargs.get("primary_model", "gsf").lower() 952 | interaction_model = ( 953 | kwargs.get("interaction_model", "sibyll23c") 954 | .replace("-", "") 955 | .replace(".", "") 956 | .lower() 957 | ) 958 | location = kwargs.get("location", "USStd") 959 | month = kwargs.get("month", None) 960 | 961 | if output: 962 | 963 | if file_name == "" or not isinstance(file_name, str): 964 | 965 | file_name = os.path.join( 966 | constants.get_directory(), 967 | "surface", 968 | "surface_energy_spectrum_{0}_{1}_{2}_{3}.txt".format( 969 | location, str(month), interaction_model, primary_model 970 | ), 971 | ) 972 | 973 | elif model is None: 974 | 975 | if output: 976 | 977 | if file_name == "" or not isinstance(file_name, str): 978 | 979 | file_name = os.path.join( 980 | constants.get_directory(), "surface", "surface_energy_spectrum.txt" 981 | ) 982 | 983 | else: 984 | 985 | NotImplementedError( 986 | 'Model of type {0} is not available. The only options are "daemonflux" and "mceq".'.format( 987 | model 988 | ) 989 | ) 990 | 991 | # Get the surface flux matrix 992 | 993 | if constants.get_verbose() > 1: 994 | print("Calculating surface energy spectrum.") 995 | 996 | if s_fluxes is None: 997 | 998 | s_fluxes = load_s_fluxes_from_file(force=force, **kwargs) 999 | 1000 | # Check that the surface flux matrix has been loaded properly 1001 | 1002 | if s_fluxes is None: 1003 | raise Exception( 1004 | "Surface intensities not calculated. The surface flux matrix was not provided or loaded correctly." 1005 | ) 1006 | 1007 | s_fluxes = np.atleast_2d(s_fluxes) 1008 | 1009 | # Calculate the surface energy spectrum 1010 | 1011 | s_e_spect = ( 1012 | 2 1013 | * np.pi 1014 | * abs( 1015 | scii.simpson( 1016 | s_fluxes, x=np.cos(np.radians(constants.ANGLES_FOR_S_FLUXES)), axis=1 1017 | ) 1018 | ) 1019 | ) 1020 | 1021 | if constants.get_verbose() > 1: 1022 | print("Finished calculating surface energy spectrum.") 1023 | 1024 | # Write the results to the file 1025 | 1026 | if output: 1027 | 1028 | file_out = open(file_name, "w") 1029 | 1030 | for i in range(len(constants.ENERGIES)): 1031 | 1032 | file_out.write( 1033 | "{0:1.14f} {1:1.14e}\n".format(constants.ENERGIES[i], s_e_spect[i]) 1034 | ) 1035 | 1036 | file_out.close() 1037 | 1038 | if constants.get_verbose() > 1: 1039 | print(f"Surface energy spectrum written to {file_name}.") 1040 | 1041 | return s_e_spect 1042 | 1043 | 1044 | # Calculate total surface fluxes 1045 | 1046 | 1047 | def calc_s_tot_flux( 1048 | s_fluxes=None, 1049 | model="mceq", 1050 | force=False, 1051 | **kwargs, 1052 | ): 1053 | """ 1054 | Calculate a total surface flux in units of [(cm^2 s)^-1] for default surface energy grid and surface flux zenith angles. 1055 | 1056 | The default surface energy grid is given by constants.ENERGIES, and the default zenith angles are given by constants.ANGLES_FOR_S_FLUXES. 1057 | 1058 | Parameters 1059 | ---------- 1060 | s_fluxes : NumPy ndarray, optional (default: taken from surface.load_s_fluxes_from_file()) 1061 | A surface flux matrix of shape (91, 20). 1062 | 1063 | model : str in {"daemonflux", "mceq"}, optional (default: "mceq") 1064 | The model to use to calculate surface fluxes. MCeq provides the option to specify primary, interaction, and density model keyword arguments (see Other Parameters), while daemonflux uses "gsf" as the primary model and "DDM" as the interaction model. The default model will be changed to "daemonflux" in v3.1.0. This parameter is case-insensitive. 1065 | 1066 | force : bool, optional (default: False) 1067 | If True, force the calculation of new arrays or matrices and the creation of new directories if required. 1068 | 1069 | Other Parameters 1070 | ---------------- 1071 | return_error : bool, optional (default: False) 1072 | If True, if model == "daemonflux", return the error on the surface fluxes instead of the surface fluxes. 1073 | 1074 | primary_model : str in {"gsf", "hg", "h3a", "h4a", "gh", "gst3", "gst4", "zs", "zsp", "pl27"} or tuple, optional (default: "gsf") 1075 | The primary flux model to use in MCEq. This parameter is case-insensitive. Options: 1076 | gsf = GlobalSplineFitBeta 1077 | hg = HillasGaisser2012 (H3a) 1078 | h3a = HillasGaisser2012 (H3a) 1079 | h4a = HillasGaisser2012 (H4a) 1080 | gh = GaisserHonda 1081 | gst3 = GaisserStanevTilav (3-gen) 1082 | gst4 = GaisserStanevTilav (4-gen) 1083 | zs = Zatsepin-Sokolskaya (Default) 1084 | zsp = Zatsepin-Sokolskaya (PAMELA) 1085 | pl27 = SimplePowerlaw27 1086 | Alternatively, this can be set with a tuple. For example: (pm.GaisserStanevTilav, "3-gen"). 1087 | 1088 | interaction_model : str, optional (default: "sibyll23c") 1089 | The hadronic interaction model to use in MCEq. See the Tutorial or MCEq documentation for a list of options. This parameter is case-insensitive. 1090 | 1091 | atmosphere : {"corsika", "msis00"}, optional (default: "corsika") 1092 | The atmospheric model to use in MCEq. For US Standard Atmosphere, use "corsika". For seasonal variations, use "msis00". This parameter is case-insensitive. 1093 | 1094 | location : str or tuple, optional (default: "USStd") 1095 | The name of the location for which to calculate the surface fluxes. See the Tutorial or MCEq documentation for a list of options. Alternatively, this can be set with a tuple of shape (latitude, longitude). This parameter is case-sensitive. 1096 | 1097 | month : str, optional (default: None) 1098 | The month for which to calculate the surface fluxes. For US Standard Atmosphere, use None. For seasonal variations, use the month name. This parameter is case-sensitive. 1099 | 1100 | Returns 1101 | ------- 1102 | s_tot_flux : float 1103 | The total surface flux in units of [(cm^2 s)^-1]. 1104 | """ 1105 | 1106 | # Check values 1107 | 1108 | constants._check_constants() 1109 | 1110 | # Calculate the surface intensities 1111 | 1112 | if constants.get_verbose() > 1: 1113 | print("Calculating total surface flux.") 1114 | 1115 | s_intensities = calc_s_intensities( 1116 | s_fluxes=s_fluxes, model=model, force=force, **kwargs 1117 | ) 1118 | 1119 | if s_intensities is None: 1120 | raise Exception( 1121 | "Total surface flux not calculated. The surface intensities were not calculated properly." 1122 | ) 1123 | 1124 | # Calculate the total surface flux 1125 | 1126 | s_tot_flux = float( 1127 | 2 1128 | * np.pi 1129 | * abs( 1130 | scii.simpson( 1131 | s_intensities, x=np.cos(np.radians(constants.ANGLES_FOR_S_FLUXES)) 1132 | ) 1133 | ) 1134 | ) 1135 | 1136 | if constants.get_verbose() > 1: 1137 | print("Finished calculating total surface flux.") 1138 | 1139 | return float(s_tot_flux) 1140 | -------------------------------------------------------------------------------- /docs/Tutorial.md: -------------------------------------------------------------------------------- 1 | # **Tutorial** 2 | 3 | ## Table of Contents 4 | 5 | 1. [Short Example](#short-example) 6 | 2. [Importing All MUTE Modules](#importing-all-mute-modules) 7 | 3. [Changing the Constants](#changing-the-constants) 8 | 4. [Units](#units) 9 | 5. [Slant Depths and Angles](#slant-depths-and-angles) 10 | 6. [Calculating Underground Intensities](#calculating-underground-intensities) 11 | 7. [Calculating Underground Fluxes](#calculating-underground-fluxes) 12 | 8. [Calculating Underground Angular Distributions](#calculating-underground-angular-distributions) 13 | 9. [Calculating Underground Energy Spectra](#calculating-underground-energy-spectra) 14 | 10. [Calculating Mean and Median Underground Energies](#calculating-mean-and-median-underground-energies) 15 | 11. [Calculating Total Underground Fluxes](#calculating-total-underground-fluxes) 16 | 12. [Calculating Underground Depths](#calculating-underground-depths) 17 | 13. [Calculating Surface Quantities](#calculating-surface-quantities) 18 | 14. [Calculating Survival Probabilities](#calculating-survival-probabilities) 19 | 20 | ## Short Example 21 | 22 | The nearly-minimal code below will calculate an array of true vertical underground intensities using daemonflux for an array of 28 default slant depths and will plot them against the slant depths. 23 | 24 | ```python 25 | # Import packages 26 | 27 | import matplotlib.pyplot as plt 28 | 29 | import mute.constants as mtc 30 | import mute.underground as mtu 31 | 32 | # Set the constants 33 | 34 | mtc.set_overburden("flat") 35 | mtc.set_medium("rock") 36 | mtc.set_density(2.65) 37 | 38 | # Calculate true vertical intensities for the default slant depths and atmosphere 39 | 40 | intensities = mtu.calc_u_intensities(method = "tr", model = "daemonflux") 41 | intensities_errors = mtu.calc_u_intensities(method = "tr", model = "daemonflux", return_error = True) 42 | 43 | # Plot the results 44 | 45 | plt.figure(facecolor = "white") 46 | plt.semilogy(mtc.slant_depths, intensities, color = "#b82b3d") 47 | plt.fill_between(mtc.slant_depths, intensities + intensities_errors, intensities - intensities_errors, color = "#b82b3d", lw = 0, alpha = 0.2) 48 | plt.xlabel(r"Slant Depth, $X$ (km.w.e.)") 49 | plt.ylabel(r"True Vertical Intensity, $I^u_{\mathrm{tr}}$ (cm$^{-2}$s$^{-1}$sr$^{-1}$)") 50 | plt.show() 51 | ``` 52 | 53 | ![Short Example Plot](short_example_plot.png) 54 | 55 | As of [v2.0.0](https://github.com/wjwoodley/mute/releases/tag/2.0.0), MUTE provides calculations for labs under both flat overburdens and mountains. 56 | 57 | ## Importing All MUTE Modules 58 | 59 | The MUTE constants and functions are split between four modules. They can be imported as follows: 60 | 61 | ```python 62 | import mute.constants as mtc 63 | import mute.surface as mts 64 | import mute.propagation as mtp 65 | import mute.underground as mtu 66 | ``` 67 | 68 | When MUTE is imported for the first time, the data files supplied with the GitHub release will be downloaded to the directory MUTE is installed to. These data files help speed up MUTE calculations significantly as they contain many pre-computed surface flux matrices and survival probability tensors. 69 | 70 | ## Changing the Constants 71 | 72 | The globally-set constants are stored in the ``constants`` module, and are set using setter functions. The following piece of code sets all of the constants to their default values. 73 | 74 | ```python 75 | mtc.set_verbose(2) 76 | mtc.set_output(True) 77 | # mtc.set_directory("mute/data") 78 | mtc.set_lab("Default") 79 | mtc.set_overburden("flat") 80 | mtc.set_vertical_depth(0.5) 81 | mtc.set_medium("rock") 82 | mtc.set_reference_density(2.65) 83 | mtc.set_n_muon(1000000) 84 | ``` 85 | 86 | The docstrings in the source code for each function describe what values they can take. 87 | 88 | It is usually best to leave the directory as the default value. Data files downloaded automatically from GitHub will be stored where MUTE is installed (the location of this directory can be found by running ``pip show mute``), as will output data files created while using MUTE. The directory can be changed for ease of use on a computing cluster (see the example in [``/examples/cluster``](../examples/cluster) or the example in [Using MUTE on a Computer Cluster](Tutorial_Cluster.md)). 89 | 90 | ### Loading Mountain Profiles 91 | 92 | To define the profile of a moutain, the ``mtc.load_mountain()`` function is used. In order to use this, the overburden type must be set to a mountain with ``mtc.set_overburden("mountain")``. Calling it like this will use the defaults for all of the optional keyword arguments: 93 | 94 | ```python 95 | mtc.load_mountain(file_name, file_units = "kmwe", rock_density = None, scale = 1, max_slant_depth = 14) 96 | ``` 97 | 98 | The file name, indicating the full (relative or absolute) path of the file that contains the information for the mountain profile must be provided. The file should have three columns: 99 | 100 | 1. Zenith angle in degrees 101 | 2. Azimuthal angle in degrees 102 | 3. Slant depth in m, m.w.e., km, or km.w.e. 103 | 104 | For an example see, [``/examples/example_mountain_calculations.ipynb``](../examples/example_mountain_calculations.ipynb). 105 | 106 | ``file_units`` refers to the units of the slant depths given in the file. The units can be either ``"m"``, ``"mwe"``, ``"km"``, or ``"kmwe"``. MUTE will convert the slant depths to km.w.e. if they are not already in km.w.e. ``rock_density`` refers to the density of rock used to define the slant depths in the file. ``scale`` can be used to scale all slant depths in the file by a multiplicative factor (useful for computing uncertainties arising from rock densities). ``max_slant_depth`` defines the maximum slant depth that will be considered in the calculations; any depths in the file that are above this value will be changed to 0 and ignored. 107 | 108 | For more information on modelling mountain overburdens for specific laboratories with MUTE, see [Modelling Labs](Tutorial_Labs.md). 109 | 110 | ## Units 111 | 112 | The following are the default units used throughout MUTE: 113 | 114 | * **Energies:** MeV 115 | * **Angles:** Degrees 116 | * **Depths:** km.w.e. 117 | * **Survival Probabilities:** $(\mathrm{MeV}^2\ \mathrm{km.w.e.})^{-1}$ 118 | * **Fluxes (Flat):** $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr}\ \mathrm{MeV})^{-1}$ 119 | * **Fluxes (Mountains):** $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr}\ \mathrm{MeV}^2){^-1}$ 120 | * **Intensities (Flat):** $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr})^{-1}$ 121 | * **Intensities (Mountains):** $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr}\ \mathrm{km.w.e.})^{-1}$ 122 | * **Energy Spectra:** $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{MeV})^{-1}$ 123 | * **Angular Distributions (Theta):** $(\mathrm{cm}^2\ \mathrm{s})^{-1}$ 124 | * **Angular Distributions (Phi):** $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{rad})^{-1}$ 125 | * **Total Fluxes:** $(\mathrm{cm}^2\ \mathrm{s})^{-1}$ 126 | * **Densities:** $\mathrm{g}\ \mathrm{cm}^{-3}$ 127 | 128 | ## Slant Depths and Angles 129 | 130 | **NOTE:** The slant depths and angles in MUTE should not be changed directly. If custom values are needed, the ``depths`` and ``angles`` arguments in the ``mtu.calc_u_intensities()`` and ``mtu.calc_u_tot_flux()`` functions can be used. If these are not sufficient, the final results can be interpolated to custom depths and angles. MUTE will raise an exception if any of ``mtc.ENERGIES``, ``mtc._SLANT_DEPTHS``, ``mtc._ANGLES``, ``mtc.ANGLES_FOR_S_FLUXES``, ``mtc.slant_depths``, or ``mtc.angles`` are changed. 131 | 132 | The default slant depths and zenith angles are given by the following constants: 133 | 134 | * **``mtc._SLANT_DEPTHS``:** An array of 28 default slant depths between 0.5 km.w.e. and 14 km.w.e., going up in steps of 0.5 km.w.e., defined by ``np.linspace(0.5, 14, 28)``. 135 | ``` 136 | array([ 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. , 5.5, 137 | 6. , 6.5, 7. , 7.5, 8. , 8.5, 9. , 9.5, 10. , 10.5, 11. , 138 | 11.5, 12. , 12.5, 13. , 13.5, 14. ]) 139 | ``` 140 | * **``mtc._ANGLES``:** An array of 28 default zenith angles corresponding to the default slant depths. 141 | ``` 142 | array([ 0. , 60. , 70.52877937, 75.52248781, 78.46304097, 143 | 80.40593177, 81.7867893 , 82.81924422, 83.62062979, 84.26082952, 144 | 84.78409143, 85.21980815, 85.58827421, 85.90395624, 86.17744627, 145 | 86.4166783 , 86.62771332, 86.81526146, 86.98303869, 87.13401602, 146 | 87.27059736, 87.39474873, 87.50809363, 87.61198454, 87.70755722, 147 | 87.7957725 , 87.87744864, 87.9532869 ]) 148 | ``` 149 | * **``mtc.ANGLES_FOR_S_FLUXES``:** An array of 20 default equally-spaced zenith angles between 0 degrees and 90 degrees for use in calculating the surface fluxes. 150 | ``` 151 | array([ 0. , 4.73684211, 9.47368421, 14.21052632, 18.94736842, 152 | 23.68421053, 28.42105263, 33.15789474, 37.89473684, 42.63157895, 153 | 47.36842105, 52.10526316, 56.84210526, 61.57894737, 66.31578947, 154 | 71.05263158, 75.78947368, 80.52631579, 85.26315789, 90. ]) 155 | ``` 156 | 157 | All calculations of surface flux matrices, survival probability tensors, and undeground flux matrices and tensors are done using these default values. For calculations after these basic steps, like those for underground intensities, interpolations are done to the values in ``mtc.slant_depths`` and ``mtc.angles`` (which have default values ``mtc._SLANT_DEPTHS`` and ``mtc._ANGLES`` respectively, but change when ``mtc.set_vertical_depth()`` is used to the set the vertical depth) for flat overburdens, and ``mtc.mountain.slant_depths``, ``mtc.mountain.zenith``, and ``mtc.mountain.azimuthal`` for mountain overburdens. This is further explained below. 158 | 159 | ### Flat Overburdens 160 | 161 | For a flat overburden, when the vertical depth, $h$, is set, the values of ``mtc.slant_depths`` and ``mtc.angles`` change according to: 162 | 163 | $$h = X\cos(\theta),$$ 164 | 165 | where $X$ is the slant depth, and $\theta$ is the zenith angle. For a lab 3.1 km.w.e. underground (or underwater), the vertical depth can be set with ``mtc.set_vertical_depth(3.1)``. Then, printing ``mtc.slant_depths`` gives: 166 | 167 | ``` 168 | array([ 3.1, 3.5, 4. , 4.5, 5. , 5.5, 6. , 6.5, 7. , 7.5, 8. , 169 | 8.5, 9. , 9.5, 10. , 10.5, 11. , 11.5, 12. , 12.5, 13. , 13.5, 170 | 14. ]) 171 | ``` 172 | 173 | The angles in ``mtc.angles`` are set according to these depths as well: 174 | 175 | $$\theta=\arccos\left(\frac{h}{X}\right),$$ 176 | 177 | where $h$ is 3.1 km.w.e., and $X$ are the slant depths just above. The angles come out to: 178 | 179 | ``` 180 | array([ 0. , 27.6604499 , 39.19496743, 46.45778097, 51.68386553, 181 | 55.69234854, 58.89107749, 61.5153646 , 63.71367932, 65.58559769, 182 | 67.20096873, 68.61051667, 69.85211371, 70.95468924, 71.94076951, 183 | 72.82818485, 73.6312505 , 74.3616036 , 75.028809 , 75.64080601, 184 | 76.2042431 , 76.72473252, 77.20704627]) 185 | ``` 186 | 187 | [PROPOSAL](https://github.com/tudo-astroparticlephysics/PROPOSAL) is optimised for propagating leptons through long ranges of matter. Due to this limitation of propagating muons short distances, the minimum vertical depth in MUTE is set to 0.5 km.w.e. It is possible to set vertical depths below this, using ``mtc.shallow_extrapolation``: 188 | 189 | ```python 190 | mtc.shallow_extrapolation = True 191 | mtc.set_vertical_depth(0.1) 192 | ``` 193 | 194 | However, this is not recommended. Results that depend on extrapolation up to shallow depths are not guaranteed to be stable or correct. 195 | 196 | ### Mountains 197 | 198 | ``mtc.slant_depths`` and ``mtc.angles`` are not used for mountain overburdens. Instead, after loading the mountain profile (see [above](#loading-mountain-profiles)), MUTE makes available three new global constants in the form of a named tuple that are used to access the angles and slant depths from the mountain profile. They are: 199 | 200 | * **``mtc.mountain.zenith``:** An array of unique and sorted zenith angles from the file, in degrees. 201 | * **``mtc.mountain.azimuthal``:** An array of unique and sorted azimuthal angles from the file, in degrees. 202 | * **``mtc.mountain.slant_depths``:** A matrix of unique and sorted slant depths from the file, in km.w.e. 203 | 204 | Note that these are immutable objects. They can only be changed by changing the contents of the profile file and then reloading it through the ``mtc.load_mountain()`` function. 205 | 206 | ## Calculating Underground Intensities 207 | 208 | Underground intensities can be calculated using the function ``mtu.calc_u_intensities()``. This function takes many optional keyword arguments. 209 | 210 | There are four methods available: 211 | 212 | * **``"sd"``:** Single-differential underground intensities (see [below](#single-differential-underground-intensities)). 213 | * **``"eq"``:** Vertical-equivalent underground intensities (see [below](#vertical-equivalent-underground-intensities)). 214 | * **``"tr"``:** True vertical underground intensities (see [below](#true-vertical-underground-intensities)). 215 | * **``"dd"``:** Double-differential underground intensities (see [below](#double-differential-underground-intensities)). 216 | 217 | The first three are for flat overburdens (default: ``"sd"``), and the last one is for mountain overburdens (default: ``"dd"``). 218 | 219 | ### Single-Differential Underground Intensities 220 | 221 | Single-differential underground intensities are those defined by the following equation: 222 | 223 | $$I^u(\theta)=\int_{E_{\mathrm{th}}}^{\infty}\mathrm{d}E^u\Phi^u(E^u, \theta),$$ 224 | 225 | where $I^u(\theta)$ is the underground intensity, $E_{\mathrm{th}}$ is an energy threshold, $E^u$ is the underground energy, and $\Phi^u(E^u, \theta)$ is the underground flux. 226 | 227 | #### Calling the Function 228 | 229 | Running the line below will use the defaults for all of the arguments: 230 | 231 | ```python 232 | mtu.calc_u_intensities(method = "sd", output = None, file_name = "", force = False, u_fluxes = None, s_fluxes = None, survival_probability_tensor = None, angles = mtc.angles, E_th = 0, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 233 | ``` 234 | 235 | This returns a one-dimensional array of length ``len(angles)``. The underground intensities in the array will have units of $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr})^{-1}$. 236 | 237 | The value for ``output`` will be taken from ``mtc.get_output()``, and the value for ``file_name`` will be constructed based on the values of ``mtc.get_lab()`` and ``method``. 238 | 239 | If the surface flux or survival probability matrices that are required to calculate the underground fluxes are not found, MUTE will ask if they should be created. To skip this prompt and force them to be created if they are needed, set ``force`` to ``True``. No matter what ``force`` is set to, if a matrix is output to a file, it will always overwrite any existing file with the same name. 240 | 241 | #### Keyword Arguments 242 | 243 | The possible keyword arguments and their default values are: 244 | 245 | * **``u_fluxes``:** An underground flux tensor of shape ``(28, 91, 20)`` (corresponding to the lengths of (``mtc._SLANT_DEPTHS``, ``mtc.ENERGIES``, ``mtc.ANGLES_FOR_S_FLUXES``)). If none is given, one is calculated using the given ``s_fluxes`` matrix and / or ``survival_probability tensor``, or the set or default models and global propagation constants. 246 | * **``s_fluxes``:** A surface flux matrix of shape ``(91, 20)``. If none is given, one is loaded using the set or default arguments for the models. 247 | * **``survival_probability_tensor``:** A survival probability tensor of shape ``(91, 28, 91)``. If none is given, one is loaded using the set or default global propagation constants. 248 | * **``angles``:** An array of zenith angles in degrees to calculate the underground intensities for. If none is given, ``mtc.angles`` is used. 249 | * **``depths``:** An array of slant depths in km.w.e. to calculate the true vertical underground intensities for. If none is given, ``mtc.slant_depths`` is used. 250 | * **``E_th``:** An energy threshold in MeV to act as the lower bound of the integration of the underground fluxes. If none is given, 0 MeV is used as the lower bound. 251 | * **``model``:** The surface flux model to use ([daemonflux](https://github.com/mceq-project/daemonflux) or [MCEq](https://github.com/afedynitch/MCEq)) if surface fluxes are to be calculated or loaded (as opposed to provided by the ``s_fluxes`` parameter or implicitly included in the ``u_fluxes`` parameter). The default is ``"mceq"``. This parameter is case-insensitive. 252 | * **``return_error``:** If ``True``, the surface flux errors will be returned rather than the surface fluxes. This parameter can only be used in conjunction with ``model = "daemonflux"``. The default is ``False``. 253 | * **``primary_model``:** The primary flux model to use in MCEq. See the options in [``Tutorial_Models``](Tutorial_Models.md#Primary-Model). If none is given, ``"gsf"`` is used. This parameter is case-insensitive. 254 | * **``interaction_model``:** The hadronic interaction model to use in MCEq. See the options in [``Tutorial_Models``](Tutorial_Models.md#Interaction-Model). If none is given, ``"sibyll23c"`` is used. This parameter is case-insensitive. 255 | * **``atmosphere``:** The atmospheric model. See the options in [``Tutorial_Models``](Tutorial_Models.md#Atmospheric-Model). If none is given, ``"corsika"`` is used. This parameter is case-insensitive. 256 | * **``location``:** The name of the location for which to calculate the surface fluxes, or the (latitude, longitude) coordinates of the location. See the options in [``Tutorial_Models``](Tutorial_Models.md#Atmospheric-Model). If none is given, ``"USStd"`` is used. This parameter is case-sensitive. 257 | * **``month``:** The month for which to calculate the surface fluxes. See the options in [``Tutorial_Models``](Tutorial_Models.md#Atmospheric-Model). This parameter can only be used in conjunction with ``atmosphere = "msis00"``, otherwise it is ``None``. This parameter is case-sensitive. 258 | 259 | Multiple options are available to calculate underground intensities: different combinations of tensors can be provided from which to calculate the intensities, or different combinations of models can be provided. To calculate underground intensities using the ZatsepinSokolskaya primary model, for example, one can do: 260 | 261 | ```python 262 | u_fluxes = mtu.calc_u_fluxes(primary_model = "zs") 263 | 264 | mtu.calc_u_intensities(method = "sd", u_fluxes = u_fluxes) 265 | ``` 266 | 267 | This is equivalent to: 268 | 269 | ```python 270 | s_fluxes = mts.calc_s_fluxes(primary_model = "zs") 271 | 272 | mtu.calc_u_intensities(method = "sd", s_fluxes = s_fluxes) 273 | ``` 274 | 275 | This is equivalent to: 276 | 277 | ```python 278 | mtu.calc_u_intensities(method = "sd", model = "mceq", primary_model = "zs") 279 | ``` 280 | 281 | This is equivalent to: 282 | 283 | ```python 284 | mtu.calc_u_intensities(method = "sd", model = "mceq", primary_model = (pm.ZatsepinSokolskaya, "default")) 285 | ``` 286 | 287 | In the last call above, ``pm`` is the [``crflux.models``](https://github.com/mceq-project/crflux) package. Which is the most appropriate or efficient depends on the specific use case. For more information on changing the models, see [``Tutorial_Models``](Tutorial_Models.md), and for an example of how to set different primary models, see [``/examples/example_primary_flux_models.ipynb``](../examples/example_primary_flux_models.ipynb) 288 | 289 | The arguments are listed above in their order of precedence. For example, if both ``u_fluxes`` and ``s_fluxes`` are provided, only ``u_fluxes`` will be used, because it is higher in the list than ``s_fluxes``. As a rule of thumb for the sake of efficiency, MUTE tries to perform as few calculations as possible. 290 | 291 | If no underground flux tensor is provided, the surface flux matrix and survival probability tensor are used to calculate one. If no surface flux matrix is provided, the function calls ``mts.load_s_fluxes_from_file()``, which searches for a surface flux file that has a name matching the set models and atmosphere. If no survival probability tensor is provided, the function calls ``mtp.load_survival_probability_tensor_from_file()``, which searches for a survival probability file that has a name matching the set global parameters. If it finds the required files, it will load the surface fluxes and / or survival probabilities from those files. If it does not, it will try to calculate new surface fluxes and / or survival probabilities. 292 | 293 | #### Specifying Angles 294 | 295 | The function call above will return an array of underground muon intensities for the 28 default angles (or otherwise, depending on the value set for the vertical depth; see [above](#flat-overburdens)). The angles can be changed by passing angles in degrees (float or array-like) into the function. To return the underground intensities for 100 angles between 20 and 85 degrees, for example, one can do: 296 | 297 | ```python 298 | angles = np.linspace(20, 85, 100) 299 | 300 | mtu.calc_u_intensities(method = "sd", angles = angles) 301 | ``` 302 | 303 | The function will interpolate over the default energy and angle grids to get the underground intensities at these angles. For this reason, it is not advised to pass in an array that is finer grained than ``mtc.angles``, though it is possible. 304 | 305 | ### Vertical-Equivalent Underground Intensities 306 | 307 | Vertical-equivalent underground intensities are those defined by the following equation: 308 | 309 | $$I^u_{\mathrm{eq}}(\theta)=I^u(\theta)\cos(\theta),$$ 310 | 311 | where $I^u_{\mathrm{eq}}(\theta)$ is the vertical-equivalent underground intensity, and $I^u(\theta)$ is the single-differential underground intensity, described [above](#single-differential-underground-intensities). 312 | 313 | #### Calling the Function 314 | 315 | The function is called identically to that for single-differential underground intensities (see [above](#single-differential-underground-intensities)). The same keyword arguments are accepted, and angles are specified in the same way. The only difference is that ``method`` must be ``"eq"`` instead of ``"sd"``. Running the line below will use the defaults for all of the arguments: 316 | 317 | ```python 318 | mtu.calc_u_intensities(method = "eq", output = None, file_name = "", force = False, u_fluxes = None, s_fluxes = None, survival_probability_tensor = None, angles = mtc.angles, E_th = 0, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 319 | ``` 320 | 321 | This returns a one-dimensional array of length ``len(angles)``. The underground intensities in the array will have units of $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr})^{-1}$. 322 | 323 | For an example, see [``/examples/example_vertical_intensities.ipynb``](../examples/example_vertical_intensities.ipynb). 324 | 325 | ### True Vertical Underground Intensities 326 | 327 | True vertical underground intensities are those defined by the following equation: 328 | 329 | $$I^u_{\mathrm{tr}}(X)=\int_{E_{\mathrm{th}}}^{\infty}\mathrm{d}E^u\Phi^u(E^u, X, \theta=0),$$ 330 | 331 | where $I^u_{\mathrm{tr}}$ is the true vertical underground intensity. 332 | 333 | #### Calling the Function 334 | 335 | The function is called similarly to that for single-differential underground intensities (see [above](#single-differential-underground-intensities)). The main differences are that ``method`` must be ``"tr"`` instead of ``"sd"``, and that the function takes an optional ``depths`` argument, which takes its default values from ``mtc.slant_depths``, instead of an ``angles`` argument. Running the line below will use the defaults for all of the arguments: 336 | 337 | ```python 338 | mtu.calc_u_intensities(method = "tr", output = None, file_name = "", force = False, u_fluxes = None, s_fluxes = None, survival_probability_tensor = None, depths = mtc.slant_depths, E_th = 0, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 339 | ``` 340 | 341 | This returns a one-dimensional array of length ``len(depths)``. The underground intensities in the array will have units of $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr})^{-1}$. 342 | 343 | For an example, see [``/examples/example_vertical_intensities.ipynb``](../examples/example_vertical_intensities.ipynb). 344 | 345 | The slant depths can be changed by passing slant depths in km.w.e. (float or array-like) into the function. To return the true vertical underground intensities for 40 slant depths between 3 and 10 km.w.e., for example, one can do: 346 | 347 | ```python 348 | depths = np.linspace(3, 10, 40) 349 | 350 | mtu.calc_u_intensities(method = "tr", depths = depths) 351 | ``` 352 | 353 | The function will interpolate over the default energy and angle grids to get the true vertical underground intensities at these depths. For this reason, it is not advised to pass in an array that is finer grained than ``mtc.depths``, though it is possible. 354 | 355 | ### Double-Differential Underground Intensities 356 | 357 | Double-differential underground intensities are those defined by the following equation: 358 | 359 | $$I^u_{\mathrm{dd}}(X(\theta, \phi), \theta)=\int_{E_{\mathrm{th}}}^{\infty}\mathrm{d}E^u\Phi^u(E^u, X, \theta),$$ 360 | 361 | where $I^u_{\mathrm{dd}}(X(\theta, \phi), \theta)$ is the double-differential underground intensity, and $\phi$ is the azimuthal angle. 362 | 363 | #### Calling the Function 364 | 365 | The function is called similarly to that for single-differential underground intensities (see [above](#single-differential-underground-intensities)). The main differences are that ``method`` must be ``"dd"`` instead of ``"sd"``, and that the function takes neither an ``angles`` argument nor a ``depths`` argument. Additionally, this function can only be used when the overburden type is set to ``"mountain"``. Running the line below will use the defaults for all of the arguments: 366 | 367 | ```python 368 | mtu.calc_u_intensities(method = "dd", output = None, file_name = "", force = False, u_fluxes = None, s_fluxes = None, survival_probability_tensor = None, E_th = 0, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 369 | ``` 370 | 371 | This returns a two-dimensional array of shape ``(len(mtc.mountain.zenith), len(mtc.mountain.azimuthal))``. The underground intensities in the array will have units of $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr}\ \mathrm{km.w.e.})^{-1}$. 372 | 373 | ## Calculating Underground Fluxes 374 | 375 | Underground fluxes in MUTE are calculated by convolving the results from daemonflux or MCEq for the surface flux with the results from PROPOSAL for the survival probabilities: 376 | 377 | $$\Phi^u(E^u, X, \theta)=\sum_{E^s}\Phi^s(E^s, \theta)P(E^s, E^u, X)\left(\frac{\Delta E^s}{\Delta E^u}\right),$$ 378 | 379 | where $\Phi^u(E^u, X, \theta)$ is the underground flux, $\Phi^s(E^s, \theta)$ is the surface flux, $P(E^s, E^u, X)$ is the survival probability tensor, and $\Delta E^s$ and $\Delta E^u$ are the energy bin widths for the surface and underground energies respectively. 380 | 381 | Underground fluxes can be calculated using the ``mtu.calc_u_fluxes()`` function. Its arguments are similar to those for ``mtu.calc_u_intensities()`` (see [above](#single-differential-underground-intensities)), with some differences. There are no ``method``, ``u_fluxes``, or ``E_th`` arguments for this function. Additionally, it can take a ``full_tensor`` argument. Running the line below will use the defaults for all of the arguments: 382 | 383 | ```python 384 | mtu.calc_u_fluxes(s_fluxes = None, survival_probability_tensor = None, full_tensor = False, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None, output = None, file_name = "", force = False) 385 | ``` 386 | 387 | For a flat overburden, this function will return a two-dimensional array of shape ``(91, 28)``, where the zeroth axis is the underground energies, and the first axis is the zenith angles. The units of the underground fluxes will be $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr}\ \mathrm{MeV})^{-1}$. If the argument ``full_tensor`` is set to ``True``, the full three-dimensional array will be returned, rather than a two-dimensional array, as it is for the case of a mountain. 388 | 389 | For a mountain, this function will return a three-dimensional array of shape ``(28, 91, 20)``, where the zeroth axis is the slant depths, the first axis is the underground energies, and the second axis is the zenith angles (from ``mtc.ANGLES_FOR_S_FLUXES``). The units of the underground fluxes will be $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr}\ \mathrm{MeV}^2)^{-1}$. 390 | 391 | For an example, see [``/examples/example_underground_flux.ipynb``](../examples/example_underground_flux.ipynb). 392 | 393 | ## Calculating Underground Angular Distributions 394 | 395 | Underground angular distributions are calculated by integrating the underground intensities over either the azimuthal angles, for the zenith angular distribution, or the zenith angle, for the azimuthal angular distribution. For the zenith angular distribution: 396 | 397 | $$\Phi_{\phi}^{u}(\theta)=\int_{\phi_{\mathrm{min}}}^{\phi_{\mathrm{max}}}\mathrm{d}\phi I^u_{\mathrm{dd}}(X(\theta, \phi), \theta),$$ 398 | 399 | where $\Phi_{\phi}^{u}(\theta)$ is the underground zenith angular distribution, and $\phi_{\mathrm{min}}$ and $\phi_{\mathrm{max}}$ are, respectively, the minimum and maximum azimuthal angles from the mountain profile file. Note that the subscript on $\Phi^u$ denotes the variable being integrated over. For the azimuthal angular distribution: 400 | 401 | $$\Phi_{\theta}^{u}(\phi)=\int_{c_{\mathrm{min}}}^{c_{\mathrm{max}}}\mathrm{d}\cos(\theta) I^u_{\mathrm{dd}}(X(\theta, \phi), \theta),$$ 402 | 403 | where $\Phi_{\theta}^{u}(\phi)$ is the underground azimuthal angular distribution, and $c_{\mathrm{min}}$ and $c_{\mathrm{max}}$ are, respectively, the cosines of the minimum and maximum zenith angles given in the mountain profile file. 404 | 405 | Underground angular distributions can be calculated using the ``mtu.calc_u_ang_dist()`` function. Its arguments are similar to those for ``mtu.calc_u_intensities()`` (see [above](#single-differential-underground-intensities)), with some differences. There is no ``method`` argument for this function, though there is a required ``kind`` argument, to specify either ``"zenith"`` (for the zenith angular distribution) or ``"azimuthal"`` (for the azimuthal angular distribution). Running the line below will use the defaults for all of the arguments: 406 | 407 | ```python 408 | mtu.calc_u_ang_dist(kind, output = None, file_name = "", force = False, s_fluxes = None, survival_probability_tensor = None, E_th = 0, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 409 | ``` 410 | 411 | This function returns an array of length ``len(mtc.mountain.zenith)`` in units of $(\mathrm{cm}^2\ \mathrm{s})^{-1}$ for a zenith angular distribution, or of length ``len(mtc.mountain.azimuthal)`` in units of $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{rad})^{-1}$ for an azimuthal angular distribution. 412 | 413 | For an example, see [``/examples/example_angular_distribution.ipynb``](../examples/example_angular_distribution.ipynb). 414 | 415 | ## Calculating Underground Energy Spectra 416 | 417 | Underground energy spectra are calculated by integrating the underground fluxes over the solid angle. For a flat overburden, symmetry is assumed for the azimuthal angle. Integration over the azimuthal angle, therefore, gives a factor of $2\pi$. The calculation, then, only deals with the integration over the zenith angle: 418 | 419 | $$\Phi_{\Omega}^{u}(E^u)=2\pi\int_0^{c_{\mathrm{max}}}\mathrm{d}\cos(\theta)\Phi^u(E^u, X(\theta, \phi), \theta),$$ 420 | 421 | where $\Phi_{\Omega}^{u}$ is the underground energy spectrum, and $c_{\mathrm{max}}$ is the cosine of the maximum zenith angle from ``angles``. 422 | 423 | For mountains, the azimuthal angles are taken from the mountain profile file. Therefore, the integration is done over both angles in the calculation: 424 | 425 | $$\Phi_{\Omega}^{u}(E^u)=\int_{\phi_{\mathrm{min}}}^{\phi_{\mathrm{max}}}\int_{c_{\mathrm{min}}}^{c_{\mathrm{max}}}\mathrm{d}\cos(\theta)\mathrm{d}\phi\Phi^u(E^u, X(\theta, \phi), \theta),$$ 426 | 427 | where $\phi_{\mathrm{min}}$, $\phi_{\mathrm{max}}$, $c_{\mathrm{min}}$, and $c_{\mathrm{max}}$ are, respectively, the minimum and maximum azimuthal and cosine of the zenith angles given in the mountain profile file. 428 | 429 | Underground energy spectra can be calculated using the ``mtu.calc_u_e_spect()`` function. Its arguments are similar to those for ``mtu.calc_u_intensities()`` (see [above](#single-differential-underground-intensities)), with some differences. There is no ``method`` argument for this function. Running the line below will use the defaults for all of the arguments: 430 | 431 | ```python 432 | mtu.calc_u_e_spect(output = None, file_name = "", force = False, s_fluxes = None, survival_probability_tensor = None, E_th = 0, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 433 | ``` 434 | 435 | For both flat overburdens and mountains, this function returns an array of length ``91`` in units of $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{MeV})^{-1}$. 436 | 437 | For an example, see [``/examples/example_energy.ipynb``](../examples/example_energy.ipynb). 438 | 439 | ## Calculating Mean and Median Underground Energies 440 | 441 | Mean underground energies are given by the first raw moment of the underground muon energy spectrum (see [above](#calculating-underground-energy-spectra)): 442 | 443 | $$\langle E^u\rangle=\frac{\int^{\infty}\_{E_{\mathrm{th}}}\mathrm{d}E^uE^u\Phi^u_{\Omega}(E^u)}{\int^{\infty}\_{E_{\mathrm{th}}}\mathrm{d}E^u\Phi^u_{\Omega}(E^u)},$$ 444 | 445 | where $\left$ is the mean underground energy, $\Phi^u_{\Omega}(E^u)$ is the underground energy spectrum, and $E_\mathrm{th}$ is an energy threshold. 446 | 447 | Mean underground energies can be calculated for a given overburden, either flat or mountain, using the ``mtu.calc_u_mean_e()`` function. Its arguments are similar to those for ``mtu.calc_u_intensities()`` (see [above](#single-differential-underground-intensities)). Running the line below will use the defaults for all of the arguments: 448 | 449 | ```python 450 | mtu.calc_u_mean_e(force = False, u_fluxes = None, s_fluxes = None, survival_probability_tensor = None, E_th = 0, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 451 | ``` 452 | 453 | For both flat overburdens and mountains, this function returns an array of six numbers, each with units of MeV. These six numbers are, in order: 454 | 455 | 1. The mean underground energy. 456 | 2. The median underground energy. 457 | 3. The positive 68% confidence interval on the median underground energy. 458 | 4. The negative 68% confidence interval on the median underground energy. 459 | 5. The positive 95% confidence interval on the median underground energy. 460 | 6. The negative 95% confidence interval on the median underground energy. 461 | 462 | For an example, see [``/examples/example_energy.ipynb``](../examples/example_energy.ipynb). 463 | 464 | ## Calculating Total Underground Fluxes 465 | 466 | Total underground fluxes are calculated by integrating the underground intensities over the angles. For a flat overburden, symmetry is assumed for the azimuthal angle. Integration over the azimuthal angle, therefore, gives a factor of $2\pi$. The calculation, then, only deals with the integration over the zenith angle: 467 | 468 | $$\Phi_{\mathrm{tot}}^{u}=2\pi\int_0^{c_{\mathrm{max}}}\mathrm{d}\cos(\theta)I^u(\theta),$$ 469 | 470 | where $\Phi_{\mathrm{tot}}^{u}$ is the total underground flux, and $c_{\mathrm{max}}$ is the cosine of the maximum zenith angle from ``angles``. 471 | 472 | For mountains, the azimuthal angles are taken from the mountain profile file. Therefore, the integration is done over both angles in the calculation: 473 | 474 | $$\Phi_{\mathrm{tot}}^{u}=\int_{\phi_{\mathrm{min}}}^{\phi_{\mathrm{max}}}\int_{c_{\mathrm{min}}}^{c_{\mathrm{max}}}\mathrm{d}\cos(\theta)\mathrm{d}\phi I^u_{\mathrm{dd}}(X(\theta, \phi), \theta),$$ 475 | 476 | where $\phi_{\mathrm{min}}$, $\phi_{\mathrm{max}}$, $c_{\mathrm{min}}$, and $c_{\mathrm{max}}$ are, respectively, the minimum and maximum azimuthal and cosine of the zenith angles given in the mountain profile file. 477 | 478 | Total underground fluxes can be calculated using the ``mtu.calc_u_tot_flux()`` function. Its arguments are similar to those for ``mtu.calc_u_intensities()`` (see [above](#single-differential-underground-intensities)), with some differences. There is no ``method`` argument for this function (for flat overburdens, it will use ``method = "sd"`` to calculate underground intensities, and, for mountains, it will use ``method = "dd"``). Additionally, it can take an ``angles`` argument, but not a ``depths`` argument. Lastly, there are no ``output`` or ``file_name`` arguments, as the output is a single number in every case. Running the line below will use the defaults for all of the arguments: 479 | 480 | ```python 481 | mtu.calc_u_tot_flux(force = False, s_fluxes = None, survival_probability_tensor = None, angles = mtc.angles, E_th = 0, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 482 | ``` 483 | 484 | For both flat overburdens and mountains, this function returns a single number (a float) in units of $(\mathrm{cm}^2\ \mathrm{s})^{-1}$. 485 | 486 | For an example, see [``/examples/total_flux_plot.ipynb``](../examples/example_total_flux_plot.ipynb). 487 | 488 | ## Calculating Underground Depths 489 | 490 | As of [v3.0.0](https://github.com/wjwoodley/mute/releases/tag/3.0.0), in addition to calculating physics observables related to the underground muon spectrum, MUTE also provides a function to calculate various types of depths. This can be done using the ``mtu.calc_depth()`` function. Because the depth is set by the user for flat overburdens, using the ``mtc.set_vertical_depth()`` function, and is therefore trivial, ``mtu.calc_depth()`` is only available for mountain overburdens. 491 | 492 | Running the line below will use the defaults for all of the arguments: 493 | 494 | ```python 495 | mtu.calc_depth(kind, u_tot_flux = None, force = False, u_fluxes = None, s_fluxes = None, survival_probability_tensor = None, E_th = 0, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 496 | ``` 497 | 498 | The ``kind`` argument is required to define the kind of depth MUTE should calculate. The possible options are: 499 | 500 | * **``ev``:** The equivalent vertical depth obtained by fitting the input or calculated total flux to a total flux curve for standard rock (see https://inspirehep.net/literature/2799258 for more details on equivalent vertical depths). 501 | * **``v``:** The straight vertical depth (engineering depth) at the center of the mountain profile (see https://inspirehep.net/literature/2799258 for more details on the engineering depth). 502 | * **``min``:** The minimum depth in the mountain profile file. 503 | * **``max``:** The maximum depth in the mountain profile file. 504 | * **``avg``:** The average depth in the mountain profile file. 505 | * **``mean``:** An alias for ``avg``. 506 | 507 | When this function is run, MUTE internally calculates an array of total flux values at various depths under standard rock, and constructs a flux vs depth curve out of them (like the one plotted [above](#short-example)). It then takes the total underground flux value input into the ``u_tot_flux`` argument, or calculates one using the default models if one is not provided, lets it float along the depth axis, fits it to the flux vs depth curve, and determines the depth for which it lands on the curve. In this way, it returns specifically a standard-rock equivalent vertical depth. 508 | 509 | In order to fit to a different flux vs depth curve (for a medium other than standard rock, for example), the function also accepts a ``survival_probability_tensor`` argument. A survival probability tensor can be loaded in using the ``mtp.load_survival_probability_tensor_from_file()`` function (see [below](#calculating-survival-probabilities)), which can then passed into the ``mtu.calc_depth()`` function through this argument. 510 | 511 | This function returns a single number (a float) in units of km.w.e. 512 | 513 | For an example of this function being used, see [``/examples/total_flux_plot.ipynb``](../examples/example_total_flux_plot.ipynb). 514 | 515 | ## Calculating Surface Quantities 516 | 517 | ### Surface Fluxes 518 | 519 | Surface flux matrices can be calculated using the ``mts.calc_s_fluxes()`` function, which makes use of [daemonflux](https://github.com/mceq-project/daemonflux) or [MCEq](https://github.com/afedynitch/MCEq). Its arguments are similar to those for ``mtu.calc_u_fluxes()`` (see [above](#calculating-underground-fluxes)), with the only differences being the absence of the ``s_fluxes`` and ``full_tensor`` arguments. Running the line below will use the defaults for all of the arguments: 520 | 521 | ```python 522 | mts.calc_s_fluxes(model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None, output = None, file_name = "", force = False, test = False) 523 | ``` 524 | 525 | This returns a two-dimensional array of shape ``(91, 20)``, where the zeroth axis is the surface energies, and the first axis is the zenith angles (from ``mtc.ANGLES_FOR_S_FLUXES``). The surface energy grid used throughout MUTE (``mtc.ENERGIES``) is the energy grid provided by MCEq, but with energies beyond 100 PeV cut, as the surface flux for energies higher than this is negligible for depths up to 14 km.w.e. 526 | 527 | To calculate surface fluxes using the HillasGaisser2012 primary flux model for Gran Sasso in January, for example, one can do: 528 | 529 | ```python 530 | mts.calc_s_fluxes(model = "mceq", primary_model = "h3a", atmosphere = "msis00", location = "SanGrasso", month = "January") 531 | ``` 532 | 533 | After a surface flux matrix has been calculated, it can be passed into other functions with the ``s_fluxes`` argument. Additionally, if an output file was produced, the matrix can be read back in from this output file using the ``mts.load_s_fluxes_from_file()`` function. This function can take either a file name, or the same arguments as ``mts.calc_s_fluxes()`` to define the models. To load the output file produced by the line of code just above, for example, one can do: 534 | 535 | ```python 536 | mts.load_s_fluxes_from_file(model = "mceq", primary_model = "h3a", atmosphere = "msis00", location = "SanGrasso", month = "January") 537 | ``` 538 | 539 | Note that the ``model``, ``primary_model``, ``interaction_model``, and ``atmosphere`` parameters are case-insensitive, while the ``location`` and ``month`` parameters are case-sensitive. 540 | 541 | ### Surface Intensities 542 | 543 | Surface intensities are single-differential intensities calculated by integrating the surface fluxes over the surface energies: 544 | 545 | $$I^s(\theta)=\int^{\infty}_0\mathrm{d}E^s\Phi^s(E^s, \theta),$$ 546 | 547 | where $I^s(\theta)$ is the surface intensity, $E^s$ is the surface energy, and $\Phi^s(E^s, \theta)$ is the surface flux. 548 | 549 | Surface intensities can be calculated using the ``mts.calc_s_intensities()`` function. Its arguments are similar to ``mts.calc_s_fluxes()`` (see [above](#surface-fluxes)), with the addition of an ``s_fluxes`` argument to pass a surface flux matrix into the function. Running the line below will use the defaults for all of the arguments: 550 | 551 | ```python 552 | mts.calc_s_intensities(s_fluxes = None, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None, output = None, file_name = "", force = False) 553 | ``` 554 | 555 | This returns a one-dimensional array of length ``20`` (from ``mtc.ANGLES_FOR_S_FLUXES``). The surface intensities in the array will have units $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{sr})^{-1}$. 556 | 557 | ### Surface Energy Spectra 558 | 559 | Surface energy spectra are calculated by integrating the suface fluxes over the solid angle. Symmetry is assumed for the azimuthal angle, and so integration over the azimuthal angle gives a factor of $2\pi$. The calculation, then, only deals with the integration over the zenith angle: 560 | 561 | $$\Phi_{\Omega}^{s}(E^u)=2\pi\int_0^{c_{\mathrm{max}}}\mathrm{d}\cos(\theta)\Phi^s(E^s, \theta),$$ 562 | 563 | where $\Phi_{\Omega}^{s}$ is the surface energy spectrum, and $c_{\mathrm{max}}$ is the cosine of the maximum zenith angle from ``angles``. 564 | 565 | Surface energy spectra can be calculated using the ``mts.calc_s_e_spect()`` function. Its arguments are very similar to those for ``mts.calc_s_fluxes()``. Running the line below will use the defaults for all of the arguments: 566 | 567 | ```python 568 | mts.calc_s_e_spect(output = None, file_name = "", force = False, s_fluxes = None, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None) 569 | ``` 570 | 571 | This function returns an array of length ``91`` in units of $(\mathrm{cm}^2\ \mathrm{s}\ \mathrm{MeV})^{-1}$. 572 | 573 | ### Total Surface Fluxes 574 | 575 | Total surface fluxes are calculated by integrating the surface intensities over the angles. At the surface, symmetry is assumed for the azimuthal angle. Integration over the azimuthal angle, therefore, gives a factor of $2\pi$. The calculation, then, only deals with the integration over the zenith angle: 576 | 577 | $$\Phi_{\mathrm{tot}}^{s}=2\pi\int_0^1\mathrm{d}\cos(\theta)I^s(\theta),$$ 578 | 579 | where $\Phi_{\mathrm{tot}}^{s}$ is the total surface flux. 580 | 581 | Total surface fluxes can be calculated using the ``mts.calc_s_tot_flux()`` function. Its arguments are similar to those for ``mts.calc_s_intensities()`` (see [above](#surface-intensities)), with the only difference being the absence of the ``output`` and ``file_name`` arguments. Running the line below will use the defaults for all of the arguments: 582 | 583 | ```python 584 | mts.calc_s_tot_fluxes(s_fluxes = None, model = "mceq", return_error = False, primary_model = "gsf", interaction_model = "sibyll23c", atmosphere = "corsika", location = "USStd", month = None, force = False) 585 | ``` 586 | 587 | This returns a single number (a float) in units of $(\mathrm{cm}^2\ \mathrm{s})^{-1}$. 588 | 589 | ## Calculating Survival Probabilities 590 | 591 | The main functions in ``mute.propagation`` used for the propagation of muons through matter and the calculation of survival probability tensors are: 592 | 593 | 1. ``mtp.propagate_muons(seed = 0, job_array_number = 0, output = None, force = False)`` 594 | 2. ``mtp.calc_survival_probability_tensor(seed = 0, file_name_pattern = None, n_job = 1, output = None, file_name = "", force = False)`` 595 | 596 | For general purposes, the second function can be used. It will check if underground (or underwater, etc.) muon energies for the set global propagation constants have been specified or already exist. If they do, it will load them and calculate survival probabilities. If they do not, it will call ``mtp.propagate_muons()``, which begins the Monte Carlo simulation using PROPOSAL. 597 | 598 | The ``mtp.calc_survival_probability_tensor()`` function will return a three-dimensional array of shape ``(91, 28, 91)``, where the zeroth axis is the surface energies, the first axis is the slant depths, and the second axis is the underground energies. The units of the survival probabilities will be $(\mathrm{MeV}^2\ \mathrm{km.w.e.})^{-1}$. 599 | 600 | After a survival probability tensor has been calculated, it can be passed into other functions with the ``survival_probability_tensor`` argument. Additionally, if an output file was produced, the tensor can be read back in from this output file using the ``mtp.load_survival_probability_tensor_from_file()`` function. This function can take an optional file name from which to load the survival probabilities: 601 | 602 | ```python 603 | mtp.load_survival_probability_tensor_from_file(file_name = "", force = False) 604 | ``` 605 | 606 | If no file name is given, one is constructed based on the set global propagation constants. 607 | 608 | For an example, see [``/examples/example_load_survival_probability_tensor.ipynb``](../examples/example_load_survival_probability_tensor.ipynb). --------------------------------------------------------------------------------