nullcal 0.2.1__tar.gz → 0.3.1__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.
Files changed (177) hide show
  1. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/codeql.yml +3 -3
  2. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/scheduled_release.yml +1 -1
  3. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/support_floor_update.yml +1 -1
  4. {nullcal-0.2.1 → nullcal-0.3.1}/.pre-commit-config.yaml +4 -4
  5. {nullcal-0.2.1 → nullcal-0.3.1}/PKG-INFO +2 -2
  6. {nullcal-0.2.1 → nullcal-0.3.1}/pyproject.toml +5 -5
  7. {nullcal-0.2.1 → nullcal-0.3.1}/scripts/lwa_leakage_study.py +6 -5
  8. {nullcal-0.2.1 → nullcal-0.3.1}/scripts/run_reference_blackjax.py +1 -1
  9. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/likelihood/recalibration_likelihood.py +77 -11
  10. nullcal-0.3.1/src/nullcal/sampler.py +228 -0
  11. nullcal-0.3.1/src/nullcal/studies/gpu_cost.py +428 -0
  12. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/generate_reference.py +20 -15
  13. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/reference/manifest.json +1 -1
  14. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/reference/posterior_manifest.json +1 -1
  15. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/test_reference_artifacts.py +43 -3
  16. {nullcal-0.2.1 → nullcal-0.3.1}/tests/likelihood/test_logdensity.py +50 -0
  17. nullcal-0.3.1/tests/studies/test_gpu_cost.py +172 -0
  18. nullcal-0.3.1/tests/test_sampler.py +74 -0
  19. {nullcal-0.2.1 → nullcal-0.3.1}/uv.lock +450 -363
  20. nullcal-0.2.1/src/nullcal/sampler.py +0 -113
  21. nullcal-0.2.1/tests/test_sampler.py +0 -41
  22. {nullcal-0.2.1 → nullcal-0.3.1}/.coderabbit.yaml +0 -0
  23. {nullcal-0.2.1 → nullcal-0.3.1}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  24. {nullcal-0.2.1 → nullcal-0.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  25. {nullcal-0.2.1 → nullcal-0.3.1}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  26. {nullcal-0.2.1 → nullcal-0.3.1}/.github/pull_request_template.md +0 -0
  27. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/ci.yml +0 -0
  28. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/documentation.yml +0 -0
  29. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/draft_release.yml +0 -0
  30. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/publish.yml +0 -0
  31. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/publish_testpypi.yml +0 -0
  32. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/release.yml +0 -0
  33. {nullcal-0.2.1 → nullcal-0.3.1}/.github/workflows/semantic_pull_request.yml +0 -0
  34. {nullcal-0.2.1 → nullcal-0.3.1}/.gitignore +0 -0
  35. {nullcal-0.2.1 → nullcal-0.3.1}/.markdownlint.yaml +0 -0
  36. {nullcal-0.2.1 → nullcal-0.3.1}/.prettierignore +0 -0
  37. {nullcal-0.2.1 → nullcal-0.3.1}/.prettierrc.yaml +0 -0
  38. {nullcal-0.2.1 → nullcal-0.3.1}/.typos.toml +0 -0
  39. {nullcal-0.2.1 → nullcal-0.3.1}/CODE_OF_CONDUCT.md +0 -0
  40. {nullcal-0.2.1 → nullcal-0.3.1}/CONTRIBUTING.md +0 -0
  41. {nullcal-0.2.1 → nullcal-0.3.1}/LICENSE +0 -0
  42. {nullcal-0.2.1 → nullcal-0.3.1}/MANIFEST.in +0 -0
  43. {nullcal-0.2.1 → nullcal-0.3.1}/README.md +0 -0
  44. {nullcal-0.2.1 → nullcal-0.3.1}/cliff.toml +0 -0
  45. {nullcal-0.2.1 → nullcal-0.3.1}/docs/.gitignore +0 -0
  46. {nullcal-0.2.1 → nullcal-0.3.1}/docs/contributing.md +0 -0
  47. {nullcal-0.2.1 → nullcal-0.3.1}/docs/dev/architecture.md +0 -0
  48. {nullcal-0.2.1 → nullcal-0.3.1}/docs/index.md +0 -0
  49. {nullcal-0.2.1 → nullcal-0.3.1}/docs/javascripts/mathjax.js +0 -0
  50. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/index.md +0 -0
  51. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/clustering/base.md +0 -0
  52. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/clustering/index.md +0 -0
  53. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/clustering/injection.md +0 -0
  54. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/clustering/precompute.md +0 -0
  55. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/clustering/single.md +0 -0
  56. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/clustering/time_frequency_map.md +0 -0
  57. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/data.md +0 -0
  58. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/index.md +0 -0
  59. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/likelihood/index.md +0 -0
  60. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/likelihood/recalibration_likelihood.md +0 -0
  61. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/metadata/index.md +0 -0
  62. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/metadata/yaml.md +0 -0
  63. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/null_stream/calibration.md +0 -0
  64. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/null_stream/index.md +0 -0
  65. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/null_stream/null_stream.md +0 -0
  66. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/null_stream/projector.md +0 -0
  67. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/null_stream/whiten.md +0 -0
  68. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/result/index.md +0 -0
  69. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/result/result.md +0 -0
  70. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/result/utils.md +0 -0
  71. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/time_frequency_transform/index.md +0 -0
  72. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/time_frequency_transform/inverse_wavelet_freq_funcs.md +0 -0
  73. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/time_frequency_transform/inverse_wavelet_time_funcs.md +0 -0
  74. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/time_frequency_transform/stft.md +0 -0
  75. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/time_frequency_transform/transform_freq_funcs.md +0 -0
  76. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/time_frequency_transform/transform_time_funcs.md +0 -0
  77. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/time_frequency_transform/utils.md +0 -0
  78. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/time_frequency_transform/wavelet_transforms.md +0 -0
  79. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/utils/index.md +0 -0
  80. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/utils/log.md +0 -0
  81. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/utils/snr.md +0 -0
  82. {nullcal-0.2.1 → nullcal-0.3.1}/docs/reference/nullcal/version.md +0 -0
  83. {nullcal-0.2.1 → nullcal-0.3.1}/docs/security.md +0 -0
  84. {nullcal-0.2.1 → nullcal-0.3.1}/docs/user-guide/installation.md +0 -0
  85. {nullcal-0.2.1 → nullcal-0.3.1}/docs/user-guide/quickstart.md +0 -0
  86. {nullcal-0.2.1 → nullcal-0.3.1}/renovate.json +0 -0
  87. {nullcal-0.2.1 → nullcal-0.3.1}/scripts/calibration_bilby_anchor.py +0 -0
  88. {nullcal-0.2.1 → nullcal-0.3.1}/scripts/diagnose_likelihood_agreement.py +0 -0
  89. {nullcal-0.2.1 → nullcal-0.3.1}/scripts/jax_wdm_verification.py +0 -0
  90. {nullcal-0.2.1 → nullcal-0.3.1}/scripts/release_version.py +0 -0
  91. {nullcal-0.2.1 → nullcal-0.3.1}/scripts/spline_resolution_study.py +0 -0
  92. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/__init__.py +0 -0
  93. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/calibration.py +0 -0
  94. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/clustering/__init__.py +0 -0
  95. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/clustering/base.py +0 -0
  96. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/clustering/injection.py +0 -0
  97. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/clustering/precompute.py +0 -0
  98. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/clustering/single.py +0 -0
  99. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/clustering/time_frequency_map.py +0 -0
  100. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/data.py +0 -0
  101. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/likelihood/__init__.py +0 -0
  102. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/metadata/__init__.py +0 -0
  103. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/metadata/yaml.py +0 -0
  104. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/null_stream/__init__.py +0 -0
  105. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/null_stream/calibration.py +0 -0
  106. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/null_stream/null_stream.py +0 -0
  107. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/null_stream/projector.py +0 -0
  108. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/null_stream/whiten.py +0 -0
  109. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/result/__init__.py +0 -0
  110. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/result/result.py +0 -0
  111. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/result/utils.py +0 -0
  112. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/studies/__init__.py +0 -0
  113. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/studies/lwa_leakage.py +0 -0
  114. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/studies/spline_resolution.py +0 -0
  115. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/time_frequency_transform/README.md +0 -0
  116. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/time_frequency_transform/__init__.py +0 -0
  117. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/time_frequency_transform/inverse_wavelet_freq_funcs.py +0 -0
  118. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/time_frequency_transform/inverse_wavelet_time_funcs.py +0 -0
  119. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/time_frequency_transform/stft.py +0 -0
  120. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/time_frequency_transform/transform_freq_funcs.py +0 -0
  121. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/time_frequency_transform/transform_time_funcs.py +0 -0
  122. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/time_frequency_transform/utils.py +0 -0
  123. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/time_frequency_transform/wavelet_transforms.py +0 -0
  124. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/utils/__init__.py +0 -0
  125. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/utils/log.py +0 -0
  126. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/utils/snr.py +0 -0
  127. {nullcal-0.2.1 → nullcal-0.3.1}/src/nullcal/version.py +0 -0
  128. {nullcal-0.2.1 → nullcal-0.3.1}/tests/.gitignore +0 -0
  129. {nullcal-0.2.1 → nullcal-0.3.1}/tests/calibration/bilby_anchor.md +0 -0
  130. {nullcal-0.2.1 → nullcal-0.3.1}/tests/calibration/test_model.py +0 -0
  131. {nullcal-0.2.1 → nullcal-0.3.1}/tests/calibration/test_posterior_summary.py +0 -0
  132. {nullcal-0.2.1 → nullcal-0.3.1}/tests/clustering/conftest.py +0 -0
  133. {nullcal-0.2.1 → nullcal-0.3.1}/tests/clustering/test_clustering_algorithm.py +0 -0
  134. {nullcal-0.2.1 → nullcal-0.3.1}/tests/clustering/test_clustering_classes.py +0 -0
  135. {nullcal-0.2.1 → nullcal-0.3.1}/tests/clustering/test_single_clustering.py +0 -0
  136. {nullcal-0.2.1 → nullcal-0.3.1}/tests/clustering/test_time_frequency_map.py +0 -0
  137. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/README.md +0 -0
  138. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/__init__.py +0 -0
  139. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/config.py +0 -0
  140. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/diagnostics/likelihood_agreement.json +0 -0
  141. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/generate_reference_inputs.py +0 -0
  142. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/pipeline.py +0 -0
  143. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/reference/artifacts.npz +0 -0
  144. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/reference/blackjax_posterior_manifest.json +0 -0
  145. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/reference/blackjax_posterior_samples.npz +0 -0
  146. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/reference/inputs.npz +0 -0
  147. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/reference/inputs_manifest.json +0 -0
  148. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/reference/posterior_samples.npz +0 -0
  149. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/test_blackjax_posterior.py +0 -0
  150. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/test_noise_log_likelihood_filter_domain.py +0 -0
  151. {nullcal-0.2.1 → nullcal-0.3.1}/tests/e2e/test_reference_posterior.py +0 -0
  152. {nullcal-0.2.1 → nullcal-0.3.1}/tests/likelihood/test_recalibration_direction.py +0 -0
  153. {nullcal-0.2.1 → nullcal-0.3.1}/tests/likelihood/test_recalibration_likelihood.py +0 -0
  154. {nullcal-0.2.1 → nullcal-0.3.1}/tests/metadata/test_yaml.py +0 -0
  155. {nullcal-0.2.1 → nullcal-0.3.1}/tests/null_stream/test_calibration_jax.py +0 -0
  156. {nullcal-0.2.1 → nullcal-0.3.1}/tests/null_stream/test_null_stream.py +0 -0
  157. {nullcal-0.2.1 → nullcal-0.3.1}/tests/null_stream/test_projector.py +0 -0
  158. {nullcal-0.2.1 → nullcal-0.3.1}/tests/null_stream/test_whiten.py +0 -0
  159. {nullcal-0.2.1 → nullcal-0.3.1}/tests/result/test_result.py +0 -0
  160. {nullcal-0.2.1 → nullcal-0.3.1}/tests/smoke_test.py +0 -0
  161. {nullcal-0.2.1 → nullcal-0.3.1}/tests/studies/test_lwa_leakage.py +0 -0
  162. {nullcal-0.2.1 → nullcal-0.3.1}/tests/studies/test_spline_resolution.py +0 -0
  163. {nullcal-0.2.1 → nullcal-0.3.1}/tests/test_blackjax_boundary.py +0 -0
  164. {nullcal-0.2.1 → nullcal-0.3.1}/tests/test_data.py +0 -0
  165. {nullcal-0.2.1 → nullcal-0.3.1}/tests/test_dependencies_are_declared.py +0 -0
  166. {nullcal-0.2.1 → nullcal-0.3.1}/tests/test_jax_wdm_verification.py +0 -0
  167. {nullcal-0.2.1 → nullcal-0.3.1}/tests/test_release_version.py +0 -0
  168. {nullcal-0.2.1 → nullcal-0.3.1}/tests/time_frequency_transform/test_jax_transform.py +0 -0
  169. {nullcal-0.2.1 → nullcal-0.3.1}/tests/time_frequency_transform/test_stft.py +0 -0
  170. {nullcal-0.2.1 → nullcal-0.3.1}/tests/time_frequency_transform/test_time_domain_transform.py +0 -0
  171. {nullcal-0.2.1 → nullcal-0.3.1}/tests/time_frequency_transform/test_transform_freq_funcs.py +0 -0
  172. {nullcal-0.2.1 → nullcal-0.3.1}/tests/time_frequency_transform/test_utils.py +0 -0
  173. {nullcal-0.2.1 → nullcal-0.3.1}/tests/time_frequency_transform/test_wavelet_transforms.py +0 -0
  174. {nullcal-0.2.1 → nullcal-0.3.1}/tests/time_frequency_transform/test_wdm_window.py +0 -0
  175. {nullcal-0.2.1 → nullcal-0.3.1}/tests/utils/test_log.py +0 -0
  176. {nullcal-0.2.1 → nullcal-0.3.1}/tests/utils/test_snr.py +0 -0
  177. {nullcal-0.2.1 → nullcal-0.3.1}/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@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
25
+ uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # 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@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
31
+ uses: github/codeql-action/autobuild@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
32
32
 
33
33
  - name: Perform CodeQL Analysis
34
- uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4
34
+ uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4
@@ -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@a8b0d224a4fdff017607079f83f01f013b9d66a6 # v0.0.20
34
+ uses: isaac-cf-wong/detect-git-changes-action@9863c3c0ba155ed10b54072f711ae8807aedbc87 # v0.0.22
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@dfc24273d70d9730ede96b5fdef09a8d7887cf04 # v0.2.10
25
+ uses: isaac-cf-wong/dependency-support-policy-action@f83b1c576c82b2a7dde5d3c38c222d68b76bb9cf # v0.2.12
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.9
20
+ rev: v0.17.0
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.3
32
+ rev: v1.51.1
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.9
38
+ rev: v3.9.10
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.20
57
+ rev: 0.13.0
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.2.1
3
+ Version: 0.3.1
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>=1.26.4
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
@@ -24,7 +24,7 @@ classifiers = [
24
24
  requires-python = ">=3.12"
25
25
  dynamic = ["version"]
26
26
  dependencies = [
27
- "numpy>=1.26.4",
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.9",
57
- "prek>=0.5.3",
56
+ "ruff>=0.17.0",
57
+ "prek>=0.5.5",
58
58
  "pytest-github-actions-annotate-failures>=0.4.2",
59
59
  "pytest-cov>=7.1.0",
60
- "pytest-mock>=3.15.1",
60
+ "pytest-mock>=3.16.0",
61
61
  "pytest>=9.1.1",
62
62
  ]
63
63
 
64
- docs = ["zensical>=0.0.65", "mkdocstrings-python>=2.0.9"]
64
+ docs = ["zensical>=0.0.69", "mkdocstrings-python>=2.0.9"]
65
65
  build = ["hatch>=1.18.1"]
66
66
  release = ["git-cliff>=2.14.2"]
67
67
 
@@ -146,7 +146,7 @@ def write_readme(path: Path, results: list[dict[str, float | int]], commit: str,
146
146
  else "; no failure occurs on the tested grid"
147
147
  )
148
148
  recommendation = (
149
- f"Use **20-{validity_maximum:.0f} Hz** as the conservative M3 LWA validity band on this grid{bracket}. "
149
+ f"Use **20-{validity_maximum:.0f} Hz** as the conservative LWA validity band on this grid{bracket}. "
150
150
  "This is a pre-check recommendation, not a measured continuous-frequency boundary."
151
151
  )
152
152
  maximum_failure_count = max(int(row["map_failure_count"]) for row in results)
@@ -168,7 +168,7 @@ The complete numerical table is `bias_vs_frequency.csv`; it also reports p99 and
168
168
  signal-minus-null statistic shift, the noise-only false-alarm fraction, ET-D PSD, and MAP convergence.
169
169
  No frequency has more than {maximum_failure_count} failed MAP fits.
170
170
 
171
- ## Validity-band recommendation for M3
171
+ ## Validity-band recommendation
172
172
 
173
173
  {recommendation}
174
174
 
@@ -216,9 +216,10 @@ signal-present versus signal-absent pair is retained for the leakage significanc
216
216
  - The sky-factor distribution, leakage SNRs, MAP biases, and recommended band are new computational
217
217
  results produced by this run. No external numerical replication was found; they remain **unanchored
218
218
  to an independent numerical reference**. Agreement between code paths is not presented as accuracy.
219
- - This pre-check is not the M4 D2 result: it uses monochromatic bins, fixed SNR, independent equal ET-D
220
- noise, a MAP rather than a posterior, and calibration parameters free at each frequency. M4 must use
221
- a broadband source, the 19-knot calibration curve, posterior coverage, more noise realizations, and
219
+ - This pre-check is not the full leakage-to-calibration-bias measurement: it uses monochromatic
220
+ bins, fixed SNR, independent equal ET-D noise, a MAP rather than a posterior, and calibration
221
+ parameters free at each frequency. That measurement must use a broadband source, the 19-knot
222
+ calibration curve, posterior coverage, more noise realizations, and
222
223
  frequency-dependent null mitigation. The ET-D curve affects physical strain/noise scaling, while the
223
224
  fixed-SNR whitened statistics deliberately isolate the geometric frequency dependence.
224
225
 
@@ -1,4 +1,4 @@
1
- """Run the fixed BlackJAX acceptance posterior against immutable R1 inputs."""
1
+ """Run the fixed BlackJAX acceptance posterior against the immutable end-to-end reference inputs."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -169,22 +169,50 @@ class RecalibrationLikelihood:
169
169
  phase,
170
170
  )
171
171
 
172
- def _frequency_domain_null_stream(self, amplitude: jax.Array, phase: jax.Array) -> jax.Array:
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
- projector = jnp.eye(response.shape[1], dtype=response.dtype)[None, :, :] - projected_response
179
- masked_null_stream = jnp.einsum("fij,jf->if", projector, self._whitened_frequency_domain_strain)
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((self.parameter_shape[0], self._frequency_count), dtype=masked_null_stream.dtype)
182
- .at[:, self._frequency_indices]
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 _time_frequency_null_stream(self, amplitude: jax.Array, phase: jax.Array) -> jax.Array:
187
- frequency_domain = self._frequency_domain_null_stream(amplitude, phase)
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
- time_frequency = jax.vmap(transform)(frequency_domain)
201
- return jnp.where(self.time_frequency_filter[None, :, :], time_frequency, 0.0)
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
- null_stream = self._time_frequency_null_stream(amplitude, phase)
207
- return -0.5 * jnp.sum(jnp.abs(null_stream) ** 2)
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"]