nullcal 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/codeql.yml +3 -3
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/draft_release.yml +1 -1
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/release.yml +1 -1
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/scheduled_release.yml +1 -1
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/support_floor_update.yml +1 -1
- {nullcal-0.2.0 → nullcal-0.3.0}/.pre-commit-config.yaml +4 -4
- {nullcal-0.2.0 → nullcal-0.3.0}/PKG-INFO +2 -2
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/user-guide/quickstart.md +23 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/pyproject.toml +7 -6
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/calibration.py +34 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/likelihood/recalibration_likelihood.py +77 -11
- nullcal-0.3.0/src/nullcal/sampler.py +228 -0
- nullcal-0.3.0/src/nullcal/studies/gpu_cost.py +428 -0
- nullcal-0.3.0/tests/calibration/test_posterior_summary.py +23 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/likelihood/test_logdensity.py +50 -0
- nullcal-0.3.0/tests/likelihood/test_recalibration_direction.py +107 -0
- nullcal-0.3.0/tests/studies/test_gpu_cost.py +172 -0
- nullcal-0.3.0/tests/test_sampler.py +74 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/time_frequency_transform/test_time_domain_transform.py +16 -1
- {nullcal-0.2.0 → nullcal-0.3.0}/uv.lock +375 -312
- nullcal-0.2.0/src/nullcal/sampler.py +0 -113
- nullcal-0.2.0/tests/test_sampler.py +0 -41
- {nullcal-0.2.0 → nullcal-0.3.0}/.coderabbit.yaml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/pull_request_template.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/ci.yml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/documentation.yml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/publish.yml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/publish_testpypi.yml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.github/workflows/semantic_pull_request.yml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.gitignore +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.markdownlint.yaml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.prettierignore +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.prettierrc.yaml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/.typos.toml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/CODE_OF_CONDUCT.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/CONTRIBUTING.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/LICENSE +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/MANIFEST.in +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/README.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/cliff.toml +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/.gitignore +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/contributing.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/dev/architecture.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/javascripts/mathjax.js +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/clustering/base.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/clustering/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/clustering/injection.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/clustering/precompute.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/clustering/single.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/clustering/time_frequency_map.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/data.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/likelihood/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/likelihood/recalibration_likelihood.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/metadata/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/metadata/yaml.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/null_stream/calibration.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/null_stream/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/null_stream/null_stream.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/null_stream/projector.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/null_stream/whiten.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/result/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/result/result.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/result/utils.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/time_frequency_transform/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/time_frequency_transform/inverse_wavelet_freq_funcs.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/time_frequency_transform/inverse_wavelet_time_funcs.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/time_frequency_transform/stft.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/time_frequency_transform/transform_freq_funcs.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/time_frequency_transform/transform_time_funcs.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/time_frequency_transform/utils.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/time_frequency_transform/wavelet_transforms.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/utils/index.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/utils/log.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/utils/snr.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/reference/nullcal/version.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/security.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/docs/user-guide/installation.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/renovate.json +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/scripts/calibration_bilby_anchor.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/scripts/diagnose_likelihood_agreement.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/scripts/jax_wdm_verification.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/scripts/lwa_leakage_study.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/scripts/release_version.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/scripts/run_reference_blackjax.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/scripts/spline_resolution_study.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/clustering/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/clustering/base.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/clustering/injection.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/clustering/precompute.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/clustering/single.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/clustering/time_frequency_map.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/data.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/likelihood/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/metadata/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/metadata/yaml.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/null_stream/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/null_stream/calibration.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/null_stream/null_stream.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/null_stream/projector.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/null_stream/whiten.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/result/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/result/result.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/result/utils.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/studies/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/studies/lwa_leakage.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/studies/spline_resolution.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/time_frequency_transform/README.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/time_frequency_transform/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/time_frequency_transform/inverse_wavelet_freq_funcs.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/time_frequency_transform/inverse_wavelet_time_funcs.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/time_frequency_transform/stft.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/time_frequency_transform/transform_freq_funcs.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/time_frequency_transform/transform_time_funcs.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/time_frequency_transform/utils.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/time_frequency_transform/wavelet_transforms.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/utils/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/utils/log.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/utils/snr.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/src/nullcal/version.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/.gitignore +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/calibration/bilby_anchor.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/calibration/test_model.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/clustering/conftest.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/clustering/test_clustering_algorithm.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/clustering/test_clustering_classes.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/clustering/test_single_clustering.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/clustering/test_time_frequency_map.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/README.md +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/__init__.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/config.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/diagnostics/likelihood_agreement.json +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/generate_reference.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/generate_reference_inputs.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/pipeline.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/reference/artifacts.npz +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/reference/blackjax_posterior_manifest.json +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/reference/blackjax_posterior_samples.npz +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/reference/inputs.npz +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/reference/inputs_manifest.json +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/reference/manifest.json +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/reference/posterior_manifest.json +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/reference/posterior_samples.npz +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/test_blackjax_posterior.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/test_noise_log_likelihood_filter_domain.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/test_reference_artifacts.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/e2e/test_reference_posterior.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/likelihood/test_recalibration_likelihood.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/metadata/test_yaml.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/null_stream/test_calibration_jax.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/null_stream/test_null_stream.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/null_stream/test_projector.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/null_stream/test_whiten.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/result/test_result.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/smoke_test.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/studies/test_lwa_leakage.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/studies/test_spline_resolution.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/test_blackjax_boundary.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/test_data.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/test_dependencies_are_declared.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/test_jax_wdm_verification.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/test_release_version.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/time_frequency_transform/test_jax_transform.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/time_frequency_transform/test_stft.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/time_frequency_transform/test_transform_freq_funcs.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/time_frequency_transform/test_utils.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/time_frequency_transform/test_wavelet_transforms.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/time_frequency_transform/test_wdm_window.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/utils/test_log.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/tests/utils/test_snr.py +0 -0
- {nullcal-0.2.0 → nullcal-0.3.0}/zensical.toml +0 -0
|
@@ -22,13 +22,13 @@ jobs:
|
|
|
22
22
|
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
|
|
23
23
|
|
|
24
24
|
- name: Initialize CodeQL
|
|
25
|
-
uses: github/codeql-action/init@
|
|
25
|
+
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
|
|
26
26
|
with:
|
|
27
27
|
languages: python
|
|
28
28
|
queries: security-and-quality
|
|
29
29
|
|
|
30
30
|
- name: Autobuild
|
|
31
|
-
uses: github/codeql-action/autobuild@
|
|
31
|
+
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
|
|
32
32
|
|
|
33
33
|
- name: Perform CodeQL Analysis
|
|
34
|
-
uses: github/codeql-action/analyze@
|
|
34
|
+
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
|
|
23
23
|
- name: Generate changelog
|
|
24
24
|
id: changelog
|
|
25
|
-
uses: orhun/git-cliff-action@
|
|
25
|
+
uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
|
|
26
26
|
with:
|
|
27
27
|
config: cliff.toml
|
|
28
28
|
args: --unreleased --verbose
|
|
@@ -30,7 +30,7 @@ jobs:
|
|
|
30
30
|
|
|
31
31
|
- name: Generate changelog
|
|
32
32
|
id: changelog
|
|
33
|
-
uses: orhun/git-cliff-action@
|
|
33
|
+
uses: orhun/git-cliff-action@a9a95522b26fe6403f7bb24031f21fb573d0f5ff # v4.9.1
|
|
34
34
|
with:
|
|
35
35
|
config: cliff.toml
|
|
36
36
|
args: --current --verbose
|
|
@@ -31,7 +31,7 @@ jobs:
|
|
|
31
31
|
|
|
32
32
|
- name: Check for new commits
|
|
33
33
|
id: check_commits
|
|
34
|
-
uses: isaac-cf-wong/detect-git-changes-action@
|
|
34
|
+
uses: isaac-cf-wong/detect-git-changes-action@051225b2ef2faf2a02d61f16d1a027cdf941498d # v0.0.21
|
|
35
35
|
with:
|
|
36
36
|
use-latest-tag: true
|
|
37
37
|
|
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
|
|
23
23
|
- name: Apply support policy (spec0, from pyproject tool table)
|
|
24
24
|
id: policy
|
|
25
|
-
uses: isaac-cf-wong/dependency-support-policy-action@
|
|
25
|
+
uses: isaac-cf-wong/dependency-support-policy-action@af9e20cfdcb7d14a4c08614e981e8c2339d3da0f # v0.2.11
|
|
26
26
|
with:
|
|
27
27
|
mode: update
|
|
28
28
|
|
|
@@ -17,7 +17,7 @@ repos:
|
|
|
17
17
|
- id: check-json
|
|
18
18
|
|
|
19
19
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
20
|
-
rev: v0.16.
|
|
20
|
+
rev: v0.16.10
|
|
21
21
|
hooks:
|
|
22
22
|
- id: ruff
|
|
23
23
|
args: [--fix, --config=pyproject.toml]
|
|
@@ -29,13 +29,13 @@ repos:
|
|
|
29
29
|
- id: taplo-format
|
|
30
30
|
|
|
31
31
|
- repo: https://github.com/crate-ci/typos
|
|
32
|
-
rev: v1.50.
|
|
32
|
+
rev: v1.50.3
|
|
33
33
|
hooks:
|
|
34
34
|
- id: typos
|
|
35
35
|
args: []
|
|
36
36
|
|
|
37
37
|
- repo: https://github.com/rbubley/mirrors-prettier
|
|
38
|
-
rev: v3.9.
|
|
38
|
+
rev: v3.9.9
|
|
39
39
|
hooks:
|
|
40
40
|
- id: prettier
|
|
41
41
|
exclude: ^tests/e2e/diagnostics/likelihood_agreement\.json$
|
|
@@ -54,7 +54,7 @@ repos:
|
|
|
54
54
|
- id: actionlint
|
|
55
55
|
|
|
56
56
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
57
|
-
rev: 0.12.
|
|
57
|
+
rev: 0.12.23
|
|
58
58
|
hooks:
|
|
59
59
|
# Update the uv lockfile
|
|
60
60
|
- id: uv-lock
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nullcal
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A package to constrain calibration errors of a closed-geometry network of gravitational-wave detectors.
|
|
5
5
|
Project-URL: Source, https://github.com/Leuven-Gravity-Institute/nullcal
|
|
6
6
|
Project-URL: Tracker, https://github.com/Leuven-Gravity-Institute/nullcal/issues
|
|
@@ -37,7 +37,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
37
37
|
Requires-Python: >=3.12
|
|
38
38
|
Requires-Dist: blackjax>=1.6.2
|
|
39
39
|
Requires-Dist: jax>=0.11.1
|
|
40
|
-
Requires-Dist: numpy>=
|
|
40
|
+
Requires-Dist: numpy>=2.2.0
|
|
41
41
|
Requires-Dist: pandas>=2.3.3
|
|
42
42
|
Requires-Dist: pyyaml>=6.0.3
|
|
43
43
|
Requires-Dist: scipy>=1.16.3
|
|
@@ -77,6 +77,29 @@ log_posterior = likelihood.logdensity_fn(params)
|
|
|
77
77
|
Use `nullcal.sampler.sample_nuts` when the standard window-adapted NUTS run and
|
|
78
78
|
R-hat, ESS, and divergence diagnostics are desired together.
|
|
79
79
|
|
|
80
|
+
## 5. Summarize and Apply a Calibration Posterior
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from nullcal.calibration import posterior_median_calibration_factor
|
|
84
|
+
|
|
85
|
+
# result is the output of nullcal.sampler.sample_nuts(...).
|
|
86
|
+
frequencies = data.frequency_array[np.all(data.mask, axis=0)]
|
|
87
|
+
factor = posterior_median_calibration_factor(
|
|
88
|
+
frequencies,
|
|
89
|
+
likelihood.knot_frequencies,
|
|
90
|
+
result.samples["amplitude"],
|
|
91
|
+
result.samples["phase"],
|
|
92
|
+
)
|
|
93
|
+
corrected_strain = np.asarray(data.strain).copy()
|
|
94
|
+
corrected_strain[:, np.all(data.mask, axis=0)] /= np.asarray(factor)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The likelihood multiplies the whitened antenna response by the calibration
|
|
98
|
+
factor. Correcting observed strain uses its inverse, as above. The point
|
|
99
|
+
estimate evaluates the complex factor for every posterior sample at every
|
|
100
|
+
frequency, then takes separate medians of its real and imaginary parts.
|
|
101
|
+
Evaluating a spline from median knot values is a different estimator.
|
|
102
|
+
|
|
80
103
|
## Next Steps
|
|
81
104
|
|
|
82
105
|
- See [Installation](installation.md) for environment setup
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
requires-python = ">=3.12"
|
|
25
25
|
dynamic = ["version"]
|
|
26
26
|
dependencies = [
|
|
27
|
-
"numpy>=
|
|
27
|
+
"numpy>=2.2.0",
|
|
28
28
|
"blackjax>=1.6.2",
|
|
29
29
|
"jax>=0.11.1",
|
|
30
30
|
"scipy>=1.16.3",
|
|
@@ -53,15 +53,15 @@ Tracker = "https://github.com/Leuven-Gravity-Institute/nullcal/issues"
|
|
|
53
53
|
|
|
54
54
|
[dependency-groups]
|
|
55
55
|
dev = [
|
|
56
|
-
"ruff>=0.16.
|
|
57
|
-
"prek>=0.5.
|
|
56
|
+
"ruff>=0.16.10",
|
|
57
|
+
"prek>=0.5.4",
|
|
58
58
|
"pytest-github-actions-annotate-failures>=0.4.2",
|
|
59
59
|
"pytest-cov>=7.1.0",
|
|
60
|
-
"pytest-mock>=3.
|
|
60
|
+
"pytest-mock>=3.16.0",
|
|
61
61
|
"pytest>=9.1.1",
|
|
62
62
|
]
|
|
63
63
|
|
|
64
|
-
docs = ["zensical>=0.0.
|
|
64
|
+
docs = ["zensical>=0.0.67", "mkdocstrings-python>=2.0.9"]
|
|
65
65
|
build = ["hatch>=1.18.1"]
|
|
66
66
|
release = ["git-cliff>=2.14.2"]
|
|
67
67
|
|
|
@@ -84,7 +84,8 @@ parallel = true
|
|
|
84
84
|
source = ["src"]
|
|
85
85
|
|
|
86
86
|
[tool.coverage.report]
|
|
87
|
-
|
|
87
|
+
# Policy target: keep at least three-fifths of the package covered while retaining regression headroom.
|
|
88
|
+
fail_under = 60
|
|
88
89
|
omit = ["*/tests/*", "*/site-packages/*"]
|
|
89
90
|
|
|
90
91
|
[tool.coverage.paths]
|
|
@@ -7,6 +7,12 @@ uniform log-frequency grid and also permits nonuniform knot placement.
|
|
|
7
7
|
Importing this module enables JAX's process-wide ``jax_enable_x64`` setting.
|
|
8
8
|
Calibration inference requires float64 precision, so this side effect is an
|
|
9
9
|
intentional part of the module's public behavior.
|
|
10
|
+
|
|
11
|
+
For a posterior point estimate, evaluate the complex calibration factor for
|
|
12
|
+
each sample at each frequency, then take the componentwise median of its real
|
|
13
|
+
and imaginary parts. Interpolating median knot values gives a different curve.
|
|
14
|
+
The factor multiplies the antenna response; to correct observed strain, divide
|
|
15
|
+
the strain by the estimated factor.
|
|
10
16
|
"""
|
|
11
17
|
|
|
12
18
|
from __future__ import annotations
|
|
@@ -21,6 +27,7 @@ jax.config.update("jax_enable_x64", True)
|
|
|
21
27
|
import jax.numpy as jnp # noqa: E402
|
|
22
28
|
|
|
23
29
|
MINIMUM_CUBIC_SPLINE_KNOTS = 4
|
|
30
|
+
POSTERIOR_ARRAY_NDIM = 3
|
|
24
31
|
|
|
25
32
|
|
|
26
33
|
def _float64(values):
|
|
@@ -109,6 +116,32 @@ def calibration_factor(frequencies, knot_frequencies, amplitude, phase):
|
|
|
109
116
|
return (1.0 + delta_amplitude) * (2.0 + imaginary_phase) / (2.0 - imaginary_phase)
|
|
110
117
|
|
|
111
118
|
|
|
119
|
+
def posterior_median_calibration_factor(frequencies, knot_frequencies, amplitudes, phases):
|
|
120
|
+
"""Return the per-frequency posterior median of the complex factor.
|
|
121
|
+
|
|
122
|
+
``amplitudes`` and ``phases`` have shape ``(sample, detector, knot)``;
|
|
123
|
+
knots may have shape ``(knot,)`` or ``(detector, knot)``. The complex median
|
|
124
|
+
is defined componentwise in real and imaginary coordinates. This evaluates
|
|
125
|
+
every posterior sample before taking the median, rather than evaluating a
|
|
126
|
+
curve from median knot values.
|
|
127
|
+
"""
|
|
128
|
+
amplitudes = _float64(amplitudes)
|
|
129
|
+
phases = _float64(phases)
|
|
130
|
+
knots = _float64(knot_frequencies)
|
|
131
|
+
if amplitudes.ndim != POSTERIOR_ARRAY_NDIM or phases.shape != amplitudes.shape or amplitudes.shape[0] == 0:
|
|
132
|
+
raise ValueError("amplitudes and phases must have matching nonempty (sample, detector, knot) shapes")
|
|
133
|
+
if knots.ndim == 1:
|
|
134
|
+
knots = jnp.broadcast_to(knots, amplitudes.shape[1:])
|
|
135
|
+
if knots.shape != amplitudes.shape[1:]:
|
|
136
|
+
raise ValueError("knot_frequencies must match the detector-by-knot shape")
|
|
137
|
+
|
|
138
|
+
factors = jax.vmap(
|
|
139
|
+
jax.vmap(calibration_factor, in_axes=(None, 0, 0, 0)),
|
|
140
|
+
in_axes=(None, None, 0, 0),
|
|
141
|
+
)(frequencies, knots, amplitudes, phases)
|
|
142
|
+
return jnp.median(jnp.real(factors), axis=0) + 1j * jnp.median(jnp.imag(factors), axis=0)
|
|
143
|
+
|
|
144
|
+
|
|
112
145
|
def calibration_log_prior(
|
|
113
146
|
amplitude,
|
|
114
147
|
phase,
|
|
@@ -185,5 +218,6 @@ __all__ = [
|
|
|
185
218
|
"calibration_factor",
|
|
186
219
|
"calibration_log_prior",
|
|
187
220
|
"calibration_parameters_to_unconstrained",
|
|
221
|
+
"posterior_median_calibration_factor",
|
|
188
222
|
"unconstrained_to_calibration_parameters",
|
|
189
223
|
]
|
|
@@ -169,22 +169,50 @@ class RecalibrationLikelihood:
|
|
|
169
169
|
phase,
|
|
170
170
|
)
|
|
171
171
|
|
|
172
|
-
def
|
|
172
|
+
def _validated_strain(self, whitened_strain) -> jax.Array:
|
|
173
|
+
strain = jnp.asarray(whitened_strain)
|
|
174
|
+
expected_shape = (self.parameter_shape[0], self._frequency_count)
|
|
175
|
+
if tuple(strain.shape[-2:]) != expected_shape:
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"whitened_frequency_domain_strain must have trailing shape {expected_shape}; "
|
|
178
|
+
f"received {tuple(strain.shape)}"
|
|
179
|
+
)
|
|
180
|
+
return strain
|
|
181
|
+
|
|
182
|
+
def _calibrated_projector(self, amplitude: jax.Array, phase: jax.Array) -> jax.Array:
|
|
183
|
+
"""Build the per-frequency null projector for the calibrated response."""
|
|
173
184
|
factors = self._calibration_factor(amplitude, phase)
|
|
174
185
|
response = self._whitened_antenna_response * jnp.swapaxes(factors, 0, 1)[:, :, None]
|
|
175
186
|
response_dagger = jnp.swapaxes(jnp.conj(response), 1, 2)
|
|
176
187
|
gram = response_dagger @ response
|
|
177
188
|
projected_response = response @ jnp.linalg.solve(gram, response_dagger)
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
return jnp.eye(response.shape[1], dtype=response.dtype)[None, :, :] - projected_response
|
|
190
|
+
|
|
191
|
+
def _project(self, projector: jax.Array, selected_strain: jax.Array) -> jax.Array:
|
|
192
|
+
"""Apply the null projector to a masked frequency-domain strain."""
|
|
193
|
+
return jnp.einsum("fij,...jf->...if", projector, selected_strain)
|
|
194
|
+
|
|
195
|
+
def _frequency_domain_null_stream(
|
|
196
|
+
self, amplitude: jax.Array, phase: jax.Array, whitened_strain: jax.Array | None = None
|
|
197
|
+
) -> jax.Array:
|
|
198
|
+
if whitened_strain is None:
|
|
199
|
+
selected_strain = self._whitened_frequency_domain_strain
|
|
200
|
+
realisation_shape = selected_strain.shape[:-1]
|
|
201
|
+
else:
|
|
202
|
+
strain = self._validated_strain(whitened_strain)
|
|
203
|
+
selected_strain = strain[..., self._frequency_indices]
|
|
204
|
+
realisation_shape = strain.shape[:-1]
|
|
205
|
+
projector = self._calibrated_projector(amplitude, phase)
|
|
206
|
+
masked_null_stream = self._project(projector, selected_strain)
|
|
207
|
+
full_shape = (*realisation_shape, self._frequency_count)
|
|
180
208
|
return (
|
|
181
|
-
jnp.zeros(
|
|
182
|
-
.at[
|
|
209
|
+
jnp.zeros(full_shape, dtype=masked_null_stream.dtype)
|
|
210
|
+
.at[..., self._frequency_indices]
|
|
183
211
|
.set(masked_null_stream)
|
|
184
212
|
)
|
|
185
213
|
|
|
186
|
-
def
|
|
187
|
-
|
|
214
|
+
def _transform_to_time_frequency(self, frequency_domain: jax.Array) -> jax.Array:
|
|
215
|
+
"""Map a ``(..., detector, frequency)`` array through the WDM transform."""
|
|
188
216
|
|
|
189
217
|
def transform(detector_data):
|
|
190
218
|
return (
|
|
@@ -197,14 +225,52 @@ class RecalibrationLikelihood:
|
|
|
197
225
|
* self._wavelet_scale
|
|
198
226
|
)
|
|
199
227
|
|
|
200
|
-
|
|
201
|
-
|
|
228
|
+
leading_shape = frequency_domain.shape[:-1]
|
|
229
|
+
flattened = frequency_domain.reshape((-1, frequency_domain.shape[-1]))
|
|
230
|
+
transformed = jax.vmap(transform)(flattened)
|
|
231
|
+
return transformed.reshape(leading_shape + transformed.shape[-2:])
|
|
232
|
+
|
|
233
|
+
def _time_frequency_null_stream(
|
|
234
|
+
self, amplitude: jax.Array, phase: jax.Array, whitened_strain: jax.Array | None = None
|
|
235
|
+
) -> jax.Array:
|
|
236
|
+
frequency_domain = self._frequency_domain_null_stream(amplitude, phase, whitened_strain)
|
|
237
|
+
return jnp.where(self.time_frequency_filter, self._transform_to_time_frequency(frequency_domain), 0.0)
|
|
238
|
+
|
|
239
|
+
def _log_likelihood_from_stream(self, null_stream: jax.Array) -> jax.Array:
|
|
240
|
+
# The detector axis always sits immediately before the time and
|
|
241
|
+
# frequency axes, so summing the trailing three leaves exactly one
|
|
242
|
+
# value per leading realisation (and a scalar for a single realisation).
|
|
243
|
+
return -0.5 * jnp.sum(jnp.abs(null_stream) ** 2, axis=(-1, -2, -3))
|
|
202
244
|
|
|
203
245
|
def log_likelihood_fn(self, params: Mapping[str, jax.Array]) -> jax.Array:
|
|
204
246
|
"""Return the pure JAX null-stream log likelihood for ``params``."""
|
|
205
247
|
amplitude, phase = self._validated_parameters(params)
|
|
206
|
-
|
|
207
|
-
|
|
248
|
+
return self._log_likelihood_from_stream(self._time_frequency_null_stream(amplitude, phase))
|
|
249
|
+
|
|
250
|
+
def log_likelihood_for_strain(self, params: Mapping[str, jax.Array], whitened_frequency_domain_strain) -> jax.Array:
|
|
251
|
+
"""Evaluate the likelihood on a supplied whitened strain realisation.
|
|
252
|
+
|
|
253
|
+
``whitened_frequency_domain_strain`` has trailing shape
|
|
254
|
+
``(detector, frequency)`` and may carry any number of leading
|
|
255
|
+
realisation axes. The return value drops those leading axes and keeps
|
|
256
|
+
one log likelihood per realisation, so a ``vmap`` over the leading axis
|
|
257
|
+
yields one posterior target per realisation.
|
|
258
|
+
"""
|
|
259
|
+
amplitude, phase = self._validated_parameters(params)
|
|
260
|
+
null_stream = self._time_frequency_null_stream(amplitude, phase, whitened_frequency_domain_strain)
|
|
261
|
+
return self._log_likelihood_from_stream(null_stream)
|
|
262
|
+
|
|
263
|
+
def logdensity_for_strain(self, params: Mapping[str, jax.Array], whitened_frequency_domain_strain) -> jax.Array:
|
|
264
|
+
"""Return log prior plus the per-realisation log likelihood on supplied strain."""
|
|
265
|
+
amplitude, phase = self._validated_parameters(params)
|
|
266
|
+
return self.log_likelihood_for_strain(params, whitened_frequency_domain_strain) + calibration_log_prior(
|
|
267
|
+
amplitude,
|
|
268
|
+
phase,
|
|
269
|
+
self.amplitude_prior_mean,
|
|
270
|
+
self.amplitude_prior_sigma,
|
|
271
|
+
self.phase_prior_mean,
|
|
272
|
+
self.phase_prior_sigma,
|
|
273
|
+
)
|
|
208
274
|
|
|
209
275
|
def logdensity_fn(self, params: Mapping[str, jax.Array]) -> jax.Array:
|
|
210
276
|
"""Return normalized Gaussian log prior plus null-stream log likelihood."""
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""BlackJAX NUTS sampling with convergence diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import blackjax
|
|
9
|
+
import jax
|
|
10
|
+
import jax.numpy as jnp
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from .result import Result
|
|
14
|
+
|
|
15
|
+
MINIMUM_DIAGNOSTIC_CHAINS = 2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _jitter_position(position, key, scale: float):
|
|
19
|
+
leaves, structure = jax.tree.flatten(position)
|
|
20
|
+
keys = jax.random.split(key, len(leaves))
|
|
21
|
+
jittered = [
|
|
22
|
+
jnp.asarray(leaf) + scale * jax.random.normal(leaf_key, shape=jnp.asarray(leaf).shape)
|
|
23
|
+
for leaf, leaf_key in zip(leaves, keys, strict=True)
|
|
24
|
+
]
|
|
25
|
+
return jax.tree.unflatten(structure, jittered)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run_chain(
|
|
29
|
+
logdensity_fn: Callable[[Any], jax.Array],
|
|
30
|
+
initial_position: Mapping[str, Any],
|
|
31
|
+
chain_key: jax.Array,
|
|
32
|
+
*,
|
|
33
|
+
num_warmup: int,
|
|
34
|
+
num_samples: int,
|
|
35
|
+
target_acceptance_rate: float,
|
|
36
|
+
initial_position_jitter: float,
|
|
37
|
+
):
|
|
38
|
+
"""Adapt and sample one NUTS chain from a single jittered start."""
|
|
39
|
+
jitter_key, warmup_key, sample_key = jax.random.split(chain_key, 3)
|
|
40
|
+
chain_initial_position = _jitter_position(initial_position, jitter_key, initial_position_jitter)
|
|
41
|
+
adaptation = blackjax.window_adaptation(
|
|
42
|
+
blackjax.nuts,
|
|
43
|
+
logdensity_fn,
|
|
44
|
+
target_acceptance_rate=target_acceptance_rate,
|
|
45
|
+
)
|
|
46
|
+
adapted, _ = adaptation.run(warmup_key, chain_initial_position, num_steps=num_warmup)
|
|
47
|
+
kernel = blackjax.nuts(logdensity_fn, **adapted.parameters)
|
|
48
|
+
sample_keys = jax.random.split(sample_key, num_samples)
|
|
49
|
+
|
|
50
|
+
def step(state, key):
|
|
51
|
+
next_state, info = kernel.step(key, state)
|
|
52
|
+
return next_state, (next_state.position, next_state.logdensity, info.is_divergent, info.num_integration_steps)
|
|
53
|
+
|
|
54
|
+
_, (positions, logdensity, divergences, integration_steps) = jax.lax.scan(step, adapted.state, sample_keys)
|
|
55
|
+
return positions, logdensity, divergences, jnp.sum(integration_steps)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def sample_nuts(
|
|
59
|
+
logdensity_fn: Callable[[Any], jax.Array],
|
|
60
|
+
initial_position: Mapping[str, Any],
|
|
61
|
+
*,
|
|
62
|
+
seed: int = 0,
|
|
63
|
+
num_chains: int = 4,
|
|
64
|
+
num_warmup: int = 1_000,
|
|
65
|
+
num_samples: int = 1_000,
|
|
66
|
+
target_acceptance_rate: float = 0.8,
|
|
67
|
+
initial_position_jitter: float = 0.01,
|
|
68
|
+
) -> Result:
|
|
69
|
+
"""Adapt and run independent NUTS chains, returning mandatory diagnostics.
|
|
70
|
+
|
|
71
|
+
Chains are executed independently rather than treating detector data as a
|
|
72
|
+
realization batch. Returned parameter arrays flatten chain and draw into a
|
|
73
|
+
single leading sample dimension; chain-aware R-hat and ESS are retained in
|
|
74
|
+
``Result.metadata``.
|
|
75
|
+
"""
|
|
76
|
+
if num_chains < MINIMUM_DIAGNOSTIC_CHAINS:
|
|
77
|
+
raise ValueError("at least two chains are required to compute R-hat")
|
|
78
|
+
if num_warmup <= 0 or num_samples <= 0:
|
|
79
|
+
raise ValueError("num_warmup and num_samples must be positive")
|
|
80
|
+
if initial_position_jitter < 0.0:
|
|
81
|
+
raise ValueError("initial_position_jitter must be non-negative")
|
|
82
|
+
|
|
83
|
+
root_key = jax.random.key(seed)
|
|
84
|
+
chain_keys = jax.random.split(root_key, num_chains)
|
|
85
|
+
chain_positions = []
|
|
86
|
+
chain_logdensity = []
|
|
87
|
+
chain_divergences = []
|
|
88
|
+
chain_integration_steps = []
|
|
89
|
+
|
|
90
|
+
for chain_key in chain_keys:
|
|
91
|
+
positions, logdensity, divergences, integration_steps = _run_chain(
|
|
92
|
+
logdensity_fn,
|
|
93
|
+
initial_position,
|
|
94
|
+
chain_key,
|
|
95
|
+
num_warmup=num_warmup,
|
|
96
|
+
num_samples=num_samples,
|
|
97
|
+
target_acceptance_rate=target_acceptance_rate,
|
|
98
|
+
initial_position_jitter=initial_position_jitter,
|
|
99
|
+
)
|
|
100
|
+
chain_positions.append(positions)
|
|
101
|
+
chain_logdensity.append(logdensity)
|
|
102
|
+
chain_divergences.append(divergences)
|
|
103
|
+
chain_integration_steps.append(integration_steps)
|
|
104
|
+
|
|
105
|
+
positions_by_chain = jax.tree.map(lambda *values: jnp.stack(values), *chain_positions)
|
|
106
|
+
logdensity_by_chain = jnp.stack(chain_logdensity)
|
|
107
|
+
divergence_by_chain = jnp.stack(chain_divergences)
|
|
108
|
+
integration_steps_by_chain = jnp.stack(chain_integration_steps)
|
|
109
|
+
|
|
110
|
+
diagnostic_leaves = [value.reshape((num_chains, num_samples, -1)) for value in jax.tree.leaves(positions_by_chain)]
|
|
111
|
+
diagnostic_array = jnp.concatenate(diagnostic_leaves, axis=-1)
|
|
112
|
+
rhat = blackjax.diagnostics.rhat(diagnostic_array)
|
|
113
|
+
ess_bulk = blackjax.diagnostics.ess_bulk(diagnostic_array)
|
|
114
|
+
ess_tail = blackjax.diagnostics.ess_tail(diagnostic_array)
|
|
115
|
+
|
|
116
|
+
flattened_positions = jax.tree.map(
|
|
117
|
+
lambda value: value.reshape((num_chains * num_samples, *value.shape[2:])), positions_by_chain
|
|
118
|
+
)
|
|
119
|
+
flattened_logdensity = logdensity_by_chain.reshape(num_chains * num_samples)
|
|
120
|
+
flattened_divergence = divergence_by_chain.reshape(num_chains * num_samples)
|
|
121
|
+
metadata = {
|
|
122
|
+
"seed": seed,
|
|
123
|
+
"num_chains": num_chains,
|
|
124
|
+
"num_warmup": num_warmup,
|
|
125
|
+
"num_samples_per_chain": num_samples,
|
|
126
|
+
"target_acceptance_rate": target_acceptance_rate,
|
|
127
|
+
"max_rhat": float(np.asarray(jnp.max(rhat))),
|
|
128
|
+
"min_ess_bulk": float(np.asarray(jnp.min(ess_bulk))),
|
|
129
|
+
"min_ess_tail": float(np.asarray(jnp.min(ess_tail))),
|
|
130
|
+
"divergences": int(np.asarray(jnp.sum(divergence_by_chain))),
|
|
131
|
+
"integration_steps": int(np.asarray(jnp.sum(integration_steps_by_chain))),
|
|
132
|
+
}
|
|
133
|
+
return Result(
|
|
134
|
+
samples=flattened_positions,
|
|
135
|
+
logdensity=flattened_logdensity,
|
|
136
|
+
info={"is_divergent": flattened_divergence},
|
|
137
|
+
metadata=metadata,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def sample_nuts_batched(
|
|
142
|
+
logdensity_fn: Callable[[Any, Any], jax.Array],
|
|
143
|
+
initial_position: Mapping[str, Any],
|
|
144
|
+
realisation_data: Any,
|
|
145
|
+
*,
|
|
146
|
+
seed: int = 0,
|
|
147
|
+
num_chains_per_realisation: int = 1,
|
|
148
|
+
num_warmup: int = 1_000,
|
|
149
|
+
num_samples: int = 1_000,
|
|
150
|
+
target_acceptance_rate: float = 0.8,
|
|
151
|
+
initial_position_jitter: float = 0.01,
|
|
152
|
+
) -> Result:
|
|
153
|
+
"""Run one NUTS chain per (realisation, chain) pair under a single ``vmap``.
|
|
154
|
+
|
|
155
|
+
``logdensity_fn(position, realisation)`` evaluates one posterior target:
|
|
156
|
+
the leading axis of every leaf of ``realisation_data`` is the realisation
|
|
157
|
+
index. All chains advance inside one ``vmap``, so a campaign pays one
|
|
158
|
+
dispatch and one accelerator launch per batch instead of one per
|
|
159
|
+
realisation. The returned sample arrays flatten ``(realisation, chain,
|
|
160
|
+
draw)`` into the leading axis; the split sizes are in ``Result.metadata``.
|
|
161
|
+
|
|
162
|
+
The single-realisation :func:`sample_nuts` is the diagnostic path and
|
|
163
|
+
computes R-hat and ESS; this batched path trades those diagnostics for
|
|
164
|
+
throughput and is the cost-measurement target.
|
|
165
|
+
"""
|
|
166
|
+
if num_chains_per_realisation < 1:
|
|
167
|
+
raise ValueError("num_chains_per_realisation must be at least one")
|
|
168
|
+
if num_warmup <= 0 or num_samples <= 0:
|
|
169
|
+
raise ValueError("num_warmup and num_samples must be positive")
|
|
170
|
+
if initial_position_jitter < 0.0:
|
|
171
|
+
raise ValueError("initial_position_jitter must be non-negative")
|
|
172
|
+
|
|
173
|
+
leaves = jax.tree.leaves(realisation_data)
|
|
174
|
+
if not leaves:
|
|
175
|
+
raise ValueError("realisation_data must contain at least one array")
|
|
176
|
+
realisation_count = int(jnp.shape(leaves[0])[0]) if jnp.ndim(leaves[0]) > 0 else 0
|
|
177
|
+
if realisation_count < 1:
|
|
178
|
+
raise ValueError("realisation_data must contain at least one realisation on a leading axis")
|
|
179
|
+
|
|
180
|
+
total_chains = realisation_count * num_chains_per_realisation
|
|
181
|
+
realisation_index = jnp.repeat(jnp.arange(realisation_count), num_chains_per_realisation)
|
|
182
|
+
batched_realisations = jax.tree.map(lambda value: jnp.asarray(value)[realisation_index], realisation_data)
|
|
183
|
+
broadcast_positions = jax.tree.map(
|
|
184
|
+
lambda value: jnp.broadcast_to(jnp.asarray(value, dtype=jnp.float64), (total_chains, *jnp.shape(value))),
|
|
185
|
+
initial_position,
|
|
186
|
+
)
|
|
187
|
+
chain_keys = jax.random.split(jax.random.key(seed), total_chains)
|
|
188
|
+
|
|
189
|
+
def chain(position, chain_key, realisation):
|
|
190
|
+
return _run_chain(
|
|
191
|
+
lambda sample_position: logdensity_fn(sample_position, realisation),
|
|
192
|
+
position,
|
|
193
|
+
chain_key,
|
|
194
|
+
num_warmup=num_warmup,
|
|
195
|
+
num_samples=num_samples,
|
|
196
|
+
target_acceptance_rate=target_acceptance_rate,
|
|
197
|
+
initial_position_jitter=initial_position_jitter,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
positions_by_chain, logdensity_by_chain, divergence_by_chain, integration_steps_by_chain = jax.vmap(chain)(
|
|
201
|
+
broadcast_positions, chain_keys, batched_realisations
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
flattened_positions = jax.tree.map(
|
|
205
|
+
lambda value: value.reshape((total_chains * num_samples, *value.shape[2:])), positions_by_chain
|
|
206
|
+
)
|
|
207
|
+
flattened_logdensity = logdensity_by_chain.reshape(total_chains * num_samples)
|
|
208
|
+
flattened_divergence = divergence_by_chain.reshape(total_chains * num_samples)
|
|
209
|
+
metadata = {
|
|
210
|
+
"seed": seed,
|
|
211
|
+
"num_realisations": realisation_count,
|
|
212
|
+
"num_chains_per_realisation": num_chains_per_realisation,
|
|
213
|
+
"num_warmup": num_warmup,
|
|
214
|
+
"num_samples_per_chain": num_samples,
|
|
215
|
+
"target_acceptance_rate": target_acceptance_rate,
|
|
216
|
+
"initial_position_jitter": initial_position_jitter,
|
|
217
|
+
"divergences": int(np.asarray(jnp.sum(divergence_by_chain))),
|
|
218
|
+
"integration_steps": int(np.asarray(jnp.sum(integration_steps_by_chain))),
|
|
219
|
+
}
|
|
220
|
+
return Result(
|
|
221
|
+
samples=flattened_positions,
|
|
222
|
+
logdensity=flattened_logdensity,
|
|
223
|
+
info={"is_divergent": flattened_divergence},
|
|
224
|
+
metadata=metadata,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
__all__ = ["sample_nuts", "sample_nuts_batched"]
|