├── 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 | [](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 | 
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 | 
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).
--------------------------------------------------------------------------------