bfflearn 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.
Files changed (152) hide show
  1. {bfflearn-0.2.0 → bfflearn-0.3.0}/CHANGELOG.md +48 -0
  2. {bfflearn-0.2.0 → bfflearn-0.3.0}/CITATION.cff +2 -2
  3. {bfflearn-0.2.0 → bfflearn-0.3.0}/PKG-INFO +12 -2
  4. {bfflearn-0.2.0 → bfflearn-0.3.0}/README.md +10 -0
  5. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/__init__.py +1 -1
  6. bfflearn-0.3.0/bff/bayes/__init__.py +21 -0
  7. bfflearn-0.3.0/bff/bayes/effective_observations.py +31 -0
  8. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/bayes/gaussian_process.py +27 -9
  9. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/bayes/learning.py +21 -38
  10. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/bayes/likelihoods.py +62 -39
  11. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/cli.py +1 -118
  12. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/plotting.py +233 -0
  13. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/qoi/data.py +10 -3
  14. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/qoi/rdf.py +3 -3
  15. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/qoi/routines.py +5 -3
  16. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/topology.py +38 -11
  17. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/_shared/config.py +1 -22
  18. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/build/config.py +4 -4
  19. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/fit/config.py +6 -7
  20. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/fit/main.py +0 -4
  21. bfflearn-0.3.0/bff/workflows/learn/config.py +210 -0
  22. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/learn/main.py +90 -9
  23. {bfflearn-0.2.0 → bfflearn-0.3.0}/bfflearn.egg-info/PKG-INFO +12 -2
  24. {bfflearn-0.2.0 → bfflearn-0.3.0}/bfflearn.egg-info/SOURCES.txt +2 -1
  25. {bfflearn-0.2.0 → bfflearn-0.3.0}/bfflearn.egg-info/requires.txt +1 -1
  26. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/architecture.md +3 -2
  27. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/changelog.md +31 -1
  28. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/cli.md +11 -9
  29. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/configuration/analyze.md +3 -0
  30. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/configuration/build.md +3 -1
  31. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/configuration/fit.md +7 -4
  32. bfflearn-0.3.0/docs/configuration/learn.md +127 -0
  33. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/development.md +1 -1
  34. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/examples/acetate.md +1 -0
  35. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/examples/arbitrary-data.md +9 -1
  36. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/examples/neon-mie-lgpmd.md +12 -3
  37. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/installation.md +16 -0
  38. {bfflearn-0.2.0 → bfflearn-0.3.0}/pyproject.toml +2 -2
  39. bfflearn-0.3.0/tests/bayes/test_effective_observations.py +28 -0
  40. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/bayes/test_gaussian_process.py +61 -3
  41. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/bayes/test_learning_problem.py +9 -14
  42. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/bayes/test_likelihoods.py +73 -3
  43. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/qoi/test_data.py +34 -1
  44. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/test_examples.py +26 -0
  45. bfflearn-0.3.0/tests/test_plotting.py +128 -0
  46. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/test_topology.py +29 -1
  47. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/workflows/test_configs.py +206 -4
  48. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/workflows/test_regressions.py +48 -7
  49. bfflearn-0.2.0/bff/bayes/__init__.py +0 -11
  50. bfflearn-0.2.0/bff/data.py +0 -19
  51. bfflearn-0.2.0/bff/workflows/learn/config.py +0 -114
  52. bfflearn-0.2.0/docs/configuration/learn.md +0 -78
  53. bfflearn-0.2.0/tests/test_plotting.py +0 -53
  54. {bfflearn-0.2.0 → bfflearn-0.3.0}/CODE_OF_CONDUCT.md +0 -0
  55. {bfflearn-0.2.0 → bfflearn-0.3.0}/CONTRIBUTING.md +0 -0
  56. {bfflearn-0.2.0 → bfflearn-0.3.0}/LICENSE +0 -0
  57. {bfflearn-0.2.0 → bfflearn-0.3.0}/MANIFEST.in +0 -0
  58. {bfflearn-0.2.0 → bfflearn-0.3.0}/SECURITY.md +0 -0
  59. {bfflearn-0.2.0 → bfflearn-0.3.0}/SUPPORT.md +0 -0
  60. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/bayes/kernels.py +0 -0
  61. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/bayes/means.py +0 -0
  62. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/bayes/posterior.py +0 -0
  63. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/bayes/priors.py +0 -0
  64. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/bayes/results.py +0 -0
  65. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/bayes/utils.py +0 -0
  66. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/domain/__init__.py +0 -0
  67. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/domain/bias.py +0 -0
  68. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/domain/sample.py +0 -0
  69. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/domain/specs.py +0 -0
  70. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/__init__.py +0 -0
  71. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/colvars.py +0 -0
  72. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/commands.py +0 -0
  73. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/cp2k.py +0 -0
  74. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/extxyz.py +0 -0
  75. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/logs.py +0 -0
  76. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/mdp.py +0 -0
  77. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/plumed.py +0 -0
  78. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/progress.py +0 -0
  79. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/schedulers.py +0 -0
  80. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/io/utils.py +0 -0
  81. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/mcmc/__init__.py +0 -0
  82. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/mcmc/convergence.py +0 -0
  83. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/mcmc/proposal.py +0 -0
  84. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/mcmc/sampler.py +0 -0
  85. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/qoi/__init__.py +0 -0
  86. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/qoi/analysis.py +0 -0
  87. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/qoi/hbonds.py +0 -0
  88. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/tools.py +0 -0
  89. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/__init__.py +0 -0
  90. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/_shared/__init__.py +0 -0
  91. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/_shared/campaign.py +0 -0
  92. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/_shared/preparation.py +0 -0
  93. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/_shared/scheduler.py +0 -0
  94. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/analyze/__init__.py +0 -0
  95. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/analyze/__main__.py +0 -0
  96. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/analyze/config.py +0 -0
  97. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/analyze/main.py +0 -0
  98. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/build/__init__.py +0 -0
  99. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/build/__main__.py +0 -0
  100. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/build/main.py +0 -0
  101. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/evaluate_snapshots/__init__.py +0 -0
  102. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/evaluate_snapshots/__main__.py +0 -0
  103. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/evaluate_snapshots/config.py +0 -0
  104. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/evaluate_snapshots/main.py +0 -0
  105. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/examples.py +0 -0
  106. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/fit/__init__.py +0 -0
  107. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/fit/__main__.py +0 -0
  108. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/learn/__init__.py +0 -0
  109. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/learn/__main__.py +0 -0
  110. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/md/__init__.py +0 -0
  111. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/md/__main__.py +0 -0
  112. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/md/config.py +0 -0
  113. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/md/main.py +0 -0
  114. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/prepare_assets/__init__.py +0 -0
  115. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/prepare_assets/__main__.py +0 -0
  116. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/prepare_assets/config.py +0 -0
  117. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/prepare_assets/main.py +0 -0
  118. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/sample/__init__.py +0 -0
  119. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/sample/__main__.py +0 -0
  120. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/sample/config.py +0 -0
  121. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/sample/main.py +0 -0
  122. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/validate/__init__.py +0 -0
  123. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/validate/__main__.py +0 -0
  124. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/validate/config.py +0 -0
  125. {bfflearn-0.2.0 → bfflearn-0.3.0}/bff/workflows/validate/main.py +0 -0
  126. {bfflearn-0.2.0 → bfflearn-0.3.0}/bfflearn.egg-info/dependency_links.txt +0 -0
  127. {bfflearn-0.2.0 → bfflearn-0.3.0}/bfflearn.egg-info/entry_points.txt +0 -0
  128. {bfflearn-0.2.0 → bfflearn-0.3.0}/bfflearn.egg-info/top_level.txt +0 -0
  129. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/assets/bff-logo.pdf +0 -0
  130. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/assets/bff-logo.svg +0 -0
  131. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/configuration/evaluate-snapshots.md +0 -0
  132. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/configuration/prepare-assets.md +0 -0
  133. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/configuration/sample.md +0 -0
  134. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/configuration/validate.md +0 -0
  135. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/examples/index.md +0 -0
  136. {bfflearn-0.2.0 → bfflearn-0.3.0}/docs/index.md +0 -0
  137. {bfflearn-0.2.0 → bfflearn-0.3.0}/mkdocs.yml +0 -0
  138. {bfflearn-0.2.0 → bfflearn-0.3.0}/setup.cfg +0 -0
  139. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/bayes/test_priors_posterior.py +0 -0
  140. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/bayes/test_results.py +0 -0
  141. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/bayes/test_utils.py +0 -0
  142. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/domain/test_specs.py +0 -0
  143. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/io/_fake_mdanalysis.py +0 -0
  144. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/io/test_cp2k.py +0 -0
  145. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/io/test_cp2k_collect.py +0 -0
  146. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/io/test_logs.py +0 -0
  147. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/io/test_mdp.py +0 -0
  148. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/mcmc/test_convergence.py +0 -0
  149. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/mcmc/test_proposal.py +0 -0
  150. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/mcmc/test_sampler.py +0 -0
  151. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/workflows/test_charge_constraints.py +0 -0
  152. {bfflearn-0.2.0 → bfflearn-0.3.0}/tests/workflows/test_scheduler.py +0 -0
@@ -2,6 +2,54 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## `0.3.0` - 2026-06-11
6
+
7
+ ### Breaking Changes
8
+
9
+ - Moved effective-observation configuration from `bff fit` to each model under
10
+ `bff learn`. Model entries now require `model_path` and exactly one strategy:
11
+ explicit `n_eff`, `independent_observations: true`, or curve inference with
12
+ `tolerance`.
13
+ - Changed `.lgp` serialization to store reference-curve metadata. Models
14
+ written by earlier releases must be refitted before use with 0.3.0.
15
+
16
+ ### Added
17
+
18
+ - Added prominence-based effective-observation estimates for smooth,
19
+ curve-valued QoIs while retaining direct counts for independent scalar data.
20
+ - Added `qoi-marginals.pdf`, which colors posterior parameter marginals by the
21
+ local contribution of each QoI and shows priors and parameter bounds.
22
+ - Added per-QoI Gaussian log-likelihood evaluation for post-processing thinned
23
+ posterior samples.
24
+
25
+ ### Changed
26
+
27
+ - Made the Gaussian likelihood insensitive to curve binning by applying
28
+ `n_eff` to a mean squared residual rather than treating every bin as an
29
+ independent observation.
30
+ - Allowed surrogate models for different QoIs to use different numbers of
31
+ training rows, provided their parameter dimensions agree.
32
+ - Simplified the `bff` command entry point and shell completion while retaining
33
+ subcommands such as `bff build`.
34
+ - Made build templates optional for systems that do not require molecule
35
+ templates.
36
+ - Updated the packaged examples and documentation for effective observations
37
+ and QoI-attributed posterior plots.
38
+ - Included `pytest` in the standard installation so packaged test and example
39
+ checks are available without installing the developer extra.
40
+
41
+ ### Fixed
42
+
43
+ - Fixed multiprocessing imports for analysis routines loaded from user Python
44
+ files.
45
+
46
+ ## `0.2.1` - 2026-06-01
47
+
48
+ ### Fixed
49
+
50
+ - Restored Python 3.10 compatibility by replacing the Python 3.11-only
51
+ `typing.Self` annotation in Gaussian-process model loading.
52
+
5
53
  ## `0.2.0` - 2026-06-01
6
54
 
7
55
  The public reproduction snapshot for the published study is archived as
@@ -1,8 +1,8 @@
1
1
  cff-version: 1.2.0
2
2
  message: "If you use BFF, please cite the repository and the associated paper."
3
3
  title: "Bayesic Force Fields"
4
- version: "0.2.0"
5
- date-released: "2026-06-01"
4
+ version: "0.3.0"
5
+ date-released: "2026-06-11"
6
6
  authors:
7
7
  - family-names: Kostal
8
8
  given-names: Vojtech
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bfflearn
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Bayesian workflows for fixed-charge molecular force fields
5
5
  Author-email: Vojtech Kostal <vojtakostal8@gmail.com>
6
6
  License-Expression: GPL-3.0-only
@@ -28,6 +28,7 @@ Requires-Dist: gmxtopology>=0.1.1
28
28
  Requires-Dist: matplotlib>=3.8
29
29
  Requires-Dist: MDAnalysis>=2.7
30
30
  Requires-Dist: numpy>=1.26
31
+ Requires-Dist: pytest>=8
31
32
  Requires-Dist: PyYAML>=6.0
32
33
  Requires-Dist: scipy>=1.11
33
34
  Requires-Dist: typer>=0.12
@@ -39,7 +40,6 @@ Requires-Dist: jupyterlab; extra == "notebook"
39
40
  Requires-Dist: notebook; extra == "notebook"
40
41
  Provides-Extra: dev
41
42
  Requires-Dist: build; extra == "dev"
42
- Requires-Dist: pytest; extra == "dev"
43
43
  Requires-Dist: ruff; extra == "dev"
44
44
  Requires-Dist: twine; extra == "dev"
45
45
  Provides-Extra: docs
@@ -64,6 +64,10 @@ molecular force-field parameters from molecular-dynamics observables. It
64
64
  coordinates simulation campaigns, trajectory analysis, surrogate fitting, and
65
65
  Bayesian posterior learning.
66
66
 
67
+ Posterior learning supports explicit or data-derived effective observation
68
+ counts for scalar and curve-valued observables. It also writes QoI-attributed
69
+ marginals that show which observables support different posterior regions.
70
+
67
71
  Full documentation:
68
72
  [vojtechkostal.github.io/BayesicForceFields](https://vojtechkostal.github.io/BayesicForceFields/)
69
73
 
@@ -91,6 +95,12 @@ mamba activate bfflearn
91
95
  pip install bfflearn
92
96
  ```
93
97
 
98
+ For Jupyter notebooks, install the optional notebook tools:
99
+
100
+ ```bash
101
+ pip install "bfflearn[notebook]"
102
+ ```
103
+
94
104
  > **WARNING:** GPU-enabled PyTorch must be installed separately. Use the
95
105
  > [official PyTorch installation selector](https://pytorch.org/get-started/locally/)
96
106
  > to choose the command matching your CUDA version before running fitting,
@@ -14,6 +14,10 @@ molecular force-field parameters from molecular-dynamics observables. It
14
14
  coordinates simulation campaigns, trajectory analysis, surrogate fitting, and
15
15
  Bayesian posterior learning.
16
16
 
17
+ Posterior learning supports explicit or data-derived effective observation
18
+ counts for scalar and curve-valued observables. It also writes QoI-attributed
19
+ marginals that show which observables support different posterior regions.
20
+
17
21
  Full documentation:
18
22
  [vojtechkostal.github.io/BayesicForceFields](https://vojtechkostal.github.io/BayesicForceFields/)
19
23
 
@@ -41,6 +45,12 @@ mamba activate bfflearn
41
45
  pip install bfflearn
42
46
  ```
43
47
 
48
+ For Jupyter notebooks, install the optional notebook tools:
49
+
50
+ ```bash
51
+ pip install "bfflearn[notebook]"
52
+ ```
53
+
44
54
  > **WARNING:** GPU-enabled PyTorch must be installed separately. Use the
45
55
  > [official PyTorch installation selector](https://pytorch.org/get-started/locally/)
46
56
  > to choose the command matching your CUDA version before running fitting,
@@ -6,7 +6,7 @@ from importlib import import_module
6
6
  from pathlib import Path
7
7
  from typing import Any
8
8
 
9
- __version__ = "0.2.0"
9
+ __version__ = "0.3.0"
10
10
 
11
11
  _INTERNAL_MODULES = {
12
12
  "bayes",
@@ -0,0 +1,21 @@
1
+ from . import (
2
+ effective_observations,
3
+ gaussian_process,
4
+ kernels,
5
+ learning,
6
+ likelihoods,
7
+ priors,
8
+ results,
9
+ utils,
10
+ )
11
+
12
+ __all__ = [
13
+ 'effective_observations',
14
+ 'gaussian_process',
15
+ 'kernels',
16
+ 'learning',
17
+ 'likelihoods',
18
+ 'priors',
19
+ 'results',
20
+ 'utils',
21
+ ]
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+ from scipy.signal import find_peaks, savgol_filter
5
+
6
+
7
+ def estimate_curve_n_eff(y: np.ndarray, *, tolerance: float) -> float:
8
+ """Estimate an effective number of resolved features in one smooth curve."""
9
+ y = np.asarray(y, dtype=float)
10
+ tolerance = float(tolerance)
11
+
12
+ if y.ndim != 1 or y.size < 11:
13
+ raise ValueError("curve must be one-dimensional with at least 11 values.")
14
+ if not np.all(np.isfinite(y)):
15
+ raise ValueError("curve must contain only finite values.")
16
+ if not np.isfinite(tolerance) or tolerance <= 0.0:
17
+ raise ValueError("tolerance must be positive and finite.")
18
+
19
+ window_length = min(15, y.size if y.size % 2 else y.size - 1)
20
+ y_smooth = savgol_filter(y, window_length=window_length, polyorder=3)
21
+
22
+ noise = np.median(np.abs(y - y_smooth))
23
+ prominence_threshold = max(5.0 * noise, np.finfo(float).eps)
24
+
25
+ n_eff = 0.0
26
+ for sign in (1.0, -1.0):
27
+ _, properties = find_peaks(sign * y_smooth, prominence=prominence_threshold)
28
+ for prominence in properties["prominences"]:
29
+ n_eff += max(1.0, np.log(float(prominence) / tolerance))
30
+
31
+ return max(1.0, float(n_eff))
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Callable, Self, Union
2
+ from typing import Callable, TypeVar, Union
3
3
 
4
4
  import numpy as np
5
5
  import torch
@@ -14,6 +14,7 @@ MeanFunction = Union[
14
14
  float,
15
15
  Callable[[torch.Tensor], torch.Tensor],
16
16
  ]
17
+ LGPCommitteeT = TypeVar("LGPCommitteeT", bound="LGPCommittee")
17
18
 
18
19
 
19
20
  def evaluate_mean(
@@ -165,10 +166,10 @@ class LGPCommittee:
165
166
  ----------
166
167
  lgps : list of LocalGaussianProcess
167
168
  List of LGP models.
168
- n_observations : int
169
- Effective number of observations used in the likelihood term.
170
169
  reference_values : np.ndarray
171
170
  Reference observation vector matched by the surrogate outputs.
171
+ n_curves : int
172
+ Number of curves represented by the flattened reference vector.
172
173
  nuisance : float, optional
173
174
  Nuisance parameter for model selection (default is None).
174
175
  stochastic : bool, optional
@@ -194,15 +195,16 @@ class LGPCommittee:
194
195
  def __init__(
195
196
  self,
196
197
  lgps: list[LocalGaussianProcess],
197
- n_observations: int,
198
198
  reference_values: np.ndarray,
199
+ n_curves: int,
199
200
  nuisance: float | None = None,
200
201
  stochastic: bool = False
201
202
  ) -> None:
202
203
  self.lgps = lgps
203
204
  self.error: float | None = None
204
- self.n_observations = int(n_observations)
205
205
  self.reference_values = np.asarray(reference_values, dtype=float).reshape(-1)
206
+ self.n_curves = int(n_curves)
207
+ self.n_eff = float(self.reference_values.size)
206
208
  self.nuisance = nuisance
207
209
  self.stochastic = stochastic
208
210
 
@@ -210,6 +212,12 @@ class LGPCommittee:
210
212
  raise ValueError(
211
213
  "Reference observation size does not match surrogate output size."
212
214
  )
215
+ if self.n_curves <= 0:
216
+ raise ValueError("'n_curves' must be a positive integer.")
217
+ if self.reference_values.size % self.n_curves != 0:
218
+ raise ValueError(
219
+ "Reference observation size must be divisible by 'n_curves'."
220
+ )
213
221
 
214
222
  @property
215
223
  def size(self) -> int:
@@ -223,6 +231,10 @@ class LGPCommittee:
223
231
  def y_size(self) -> int:
224
232
  return self.lgps[0].y_size
225
233
 
234
+ @property
235
+ def curve_length(self) -> int:
236
+ return int(self.reference_values.size // self.n_curves)
237
+
226
238
  def predict(self, X: torch.Tensor) -> torch.Tensor:
227
239
  """
228
240
  Predict outputs by averaging predictions from all LGP models.
@@ -269,13 +281,19 @@ class LGPCommittee:
269
281
  return self.error
270
282
 
271
283
  @classmethod
272
- def load(cls, fn: PathLike) -> Self:
284
+ def load(cls: type[LGPCommitteeT], fn: PathLike) -> LGPCommitteeT:
273
285
  state = torch.load(fn, weights_only=False)
286
+ if "n_curves" not in state:
287
+ raise ValueError(
288
+ "This surrogate model predates BFF 0.3.0 and does not contain "
289
+ "reference-curve metadata. Refit the model before learning."
290
+ )
274
291
  lgps = [LocalGaussianProcess(**lgp_state) for lgp_state in state["lgps"]]
292
+ reference_values = np.asarray(state["reference_values"], dtype=float)
275
293
  committee = cls(
276
294
  lgps=lgps,
277
- n_observations=state["n_observations"],
278
- reference_values=np.asarray(state["reference_values"], dtype=float),
295
+ reference_values=reference_values,
296
+ n_curves=int(state["n_curves"]),
279
297
  nuisance=state["nuisance"],
280
298
  stochastic=state["stochastic"],
281
299
  )
@@ -285,8 +303,8 @@ class LGPCommittee:
285
303
  def write(self, fn_out: PathLike) -> None:
286
304
 
287
305
  state = {
288
- "n_observations": self.n_observations,
289
306
  "reference_values": self.reference_values.tolist(),
307
+ "n_curves": self.n_curves,
290
308
  "nuisance": self.nuisance,
291
309
  "stochastic": self.stochastic,
292
310
  "error": self.error,
@@ -78,20 +78,6 @@ class LearningProblem:
78
78
  + ", ".join(sorted(inconsistent_inputs))
79
79
  )
80
80
 
81
- model_input_shapes = {
82
- qoi: tuple(int(dim) for dim in model.lgps[0].X_train.shape)
83
- for qoi, model in self.models.items()
84
- }
85
- if len(set(model_input_shapes.values())) != 1:
86
- shape_summary = ", ".join(
87
- f"{qoi}={shape}"
88
- for qoi, shape in sorted(model_input_shapes.items())
89
- )
90
- raise ValueError(
91
- "All surrogate models must expect the same training input shape. "
92
- f"Found: {shape_summary}"
93
- )
94
-
95
81
  n_params = {model.n_params for model in self.models.values()}
96
82
  if len(n_params) != 1:
97
83
  raise ValueError("All surrogate models must have the same input dimension.")
@@ -114,6 +100,17 @@ class LearningProblem:
114
100
  + ", ".join(sorted(invalid))
115
101
  )
116
102
 
103
+ invalid_curve_schema = []
104
+ for qoi, model in self.models.items():
105
+ n_curves = int(getattr(model, "n_curves", 0))
106
+ if n_curves <= 0 or model.reference_values.size % n_curves != 0:
107
+ invalid_curve_schema.append(qoi)
108
+ if invalid_curve_schema:
109
+ raise ValueError(
110
+ "Models with incompatible n_curves metadata: "
111
+ + ", ".join(sorted(invalid_curve_schema))
112
+ )
113
+
117
114
  @property
118
115
  def qoi_names(self) -> list[str]:
119
116
  return list(self.models)
@@ -306,8 +303,8 @@ def fit_lgp_committee(
306
303
  test_fraction: float,
307
304
  n_hyper: int,
308
305
  committee: int,
309
- n_observations: int,
310
306
  reference_values: np.ndarray,
307
+ n_curves: int,
311
308
  nuisance: float | None,
312
309
  fn_out: PathLike | None,
313
310
  device: str,
@@ -395,10 +392,10 @@ def fit_lgp_committee(
395
392
  logger.status("Committee", f"{i}/{committee}", level=2, overwrite=True)
396
393
 
397
394
  lgp_committee = LGPCommittee(
398
- lgps,
399
- n_observations,
400
- reference_values,
401
- nuisance,
395
+ lgps=lgps,
396
+ reference_values=reference_values,
397
+ n_curves=n_curves,
398
+ nuisance=nuisance,
402
399
  )
403
400
  lgp_committee.validate(X_test, y_test)
404
401
  logger.done(
@@ -413,20 +410,11 @@ def fit_lgp_committee(
413
410
  return lgp_committee
414
411
 
415
412
 
416
- def _effective_observations(
417
- dataset: QoIDataset,
418
- observation_scale: float = 1.0,
419
- ) -> int:
420
- """Return the effective observation count used in the likelihood term."""
421
- return max(1, int(round(dataset.n_observations * float(observation_scale))))
422
-
423
-
424
413
  def fit_surrogates(
425
414
  datasets: Sequence[QoIDataset],
426
415
  *,
427
416
  y_means: Optional[Mapping[str, MeanFunction | str]] = None,
428
417
  hyperpriors: Optional[Mapping[str, Priors | Sequence]] = None,
429
- observation_scales: Optional[Mapping[str, float]] = None,
430
418
  model_paths: Optional[Mapping[str, PathLike | None]] = None,
431
419
  reuse_models: bool = True,
432
420
  n_hyper_max: int = 200,
@@ -441,7 +429,6 @@ def fit_surrogates(
441
429
  logger = logger or Logger("fit")
442
430
  y_means = dict(y_means or {})
443
431
  hyperpriors = dict(hyperpriors or {})
444
- observation_scales = dict(observation_scales or {})
445
432
  model_paths = dict(model_paths or {})
446
433
 
447
434
  if owns_logger:
@@ -456,10 +443,6 @@ def fit_surrogates(
456
443
  )
457
444
 
458
445
  qoi = dataset.name
459
- n_observations = _effective_observations(
460
- dataset,
461
- observation_scales.get(qoi, 1.0),
462
- )
463
446
  logger.info(f"QoI {qoi}", level=1)
464
447
 
465
448
  fn_model_raw = model_paths.get(qoi)
@@ -469,19 +452,20 @@ def fit_surrogates(
469
452
 
470
453
  if reuse_models and fn_model is not None and fn_model.exists():
471
454
  models[qoi] = LGPCommittee.load(fn_model)
472
- models[qoi].n_observations = n_observations
473
455
  models[qoi].reference_values = np.asarray(
474
456
  dataset.outputs_ref,
475
457
  dtype=float,
476
458
  ).reshape(-1)
459
+ models[qoi].n_eff = float(models[qoi].reference_values.size)
460
+ models[qoi].n_curves = dataset.n_curves
477
461
  if models[qoi].reference_values.size != models[qoi].y_size:
478
462
  raise ValueError(
479
463
  f"Cached surrogate for {qoi!r} is incompatible with the "
480
464
  "current reference observation size."
481
465
  )
466
+ models[qoi].write(fn_model)
482
467
  logger.info(
483
- f"Using cached surrogate model. | obs = {n_observations} "
484
- f"| MAPE = {models[qoi].error:.2f}",
468
+ f"Using cached surrogate model. | MAPE = {models[qoi].error:.2f}",
485
469
  level=2,
486
470
  )
487
471
  logger.blank()
@@ -494,8 +478,8 @@ def fit_surrogates(
494
478
  test_fraction=test_fraction,
495
479
  n_hyper=n_hyper_max,
496
480
  committee=committee_size,
497
- n_observations=n_observations,
498
481
  reference_values=dataset.outputs_ref,
482
+ n_curves=dataset.n_curves,
499
483
  nuisance=dataset.nuisance,
500
484
  fn_out=fn_model,
501
485
  device=device,
@@ -503,7 +487,6 @@ def fit_surrogates(
503
487
  opt_kwargs=opt_kwargs,
504
488
  hyperpriors=hyperpriors.get(qoi),
505
489
  )
506
- logger.kv("Effective observations", n_observations, level=2)
507
490
  logger.blank()
508
491
 
509
492
  return models
@@ -10,6 +10,42 @@ if TYPE_CHECKING:
10
10
  from .learning import LearningProblem
11
11
 
12
12
 
13
+ def _split_parameters_and_sigmas(
14
+ theta: torch.Tensor,
15
+ problem: LearningProblem,
16
+ ) -> tuple[torch.Tensor, torch.Tensor]:
17
+ """Return explicit parameters and one scalar nuisance per QoI."""
18
+ n_params = problem.n_params
19
+ params = theta[:, :n_params]
20
+ nuisance_free = theta[:, n_params:]
21
+
22
+ sigma_columns = []
23
+ j = 0
24
+ for model in problem.models.values():
25
+ if model.nuisance is None:
26
+ sigma_columns.append(nuisance_free[:, j].exp())
27
+ j += 1
28
+ else:
29
+ sigma_columns.append(
30
+ torch.full(
31
+ (len(theta),),
32
+ float(model.nuisance),
33
+ device=theta.device,
34
+ dtype=theta.dtype,
35
+ )
36
+ )
37
+ return params, torch.stack(sigma_columns, dim=1)
38
+
39
+
40
+ def _valid_parameter_mask(
41
+ params: torch.Tensor,
42
+ problem: LearningProblem,
43
+ ) -> torch.Tensor:
44
+ if problem.constraint is None:
45
+ return torch.ones(len(params), dtype=bool, device=params.device)
46
+ return problem.constraint(params)
47
+
48
+
13
49
  def loo_log_likelihood(
14
50
  theta: torch.Tensor, X: torch.Tensor, y: torch.Tensor
15
51
  ) -> torch.Tensor:
@@ -103,50 +139,37 @@ def gaussian_log_likelihood(
103
139
  Log likelihood for each sample in `theta`, shape (n_samples,).
104
140
  """
105
141
 
106
- # Assign device
107
- device = theta.device
108
-
109
- # Split the theta into parameters and nuisances
110
- n_params = problem.n_params
111
- params = theta[:, :n_params]
112
- nuisance_free = theta[:, n_params:]
142
+ contributions = gaussian_log_likelihood_by_qoi(theta, problem)
143
+ return torch.stack(tuple(contributions.values())).sum(dim=0)
113
144
 
114
- sigma_columns = []
115
- j = 0
116
- for model in problem.models.values():
117
- if model.nuisance is None:
118
- sigma_columns.append(nuisance_free[:, j].exp())
119
- j += 1
120
- else:
121
- sigma_columns.append(
122
- torch.full(
123
- (len(theta),),
124
- float(model.nuisance),
125
- device=device,
126
- dtype=theta.dtype,
127
- )
128
- )
129
- sigmas = torch.stack(sigma_columns, dim=1)
130
145
 
131
- # Check if the parameters are within the valid bounds
132
- if problem.constraint is not None:
133
- mask = problem.constraint(params)
134
- else:
135
- mask = torch.ones(len(theta), dtype=bool, device=device)
146
+ def gaussian_log_likelihood_by_qoi(
147
+ theta: torch.Tensor,
148
+ problem: LearningProblem,
149
+ ) -> dict[str, torch.Tensor]:
150
+ """Compute one Gaussian log-likelihood contribution per QoI."""
151
+ params, sigmas = _split_parameters_and_sigmas(theta, problem)
152
+ mask = _valid_parameter_mask(params, problem)
136
153
  params = params[mask]
137
154
  sigmas = sigmas[mask]
138
155
 
139
- # Compute the log-likelihod
140
- log_likelihood = torch.full((len(theta), ), -torch.inf, device=device)
141
- if mask.any():
142
- log_like_valid = torch.zeros(mask.sum(), device=device)
143
- for (qoi, model), sigma in zip(problem.models.items(), sigmas.T):
156
+ contributions = {}
157
+ for (qoi, model), sigma in zip(problem.models.items(), sigmas.T):
158
+ contribution = torch.full(
159
+ (len(theta),),
160
+ -torch.inf,
161
+ device=theta.device,
162
+ dtype=theta.dtype,
163
+ )
164
+ if mask.any():
144
165
  y_trial = model.predict(params)
145
- N = model.n_observations
146
166
  diff = problem.observations[qoi] - y_trial
147
- ssq = torch.sum(diff**2, dim=1)
148
- log_like_valid += -0.5 * ssq / sigma**2 - N * torch.log(sigma)
149
-
150
- log_likelihood[mask] = log_like_valid
167
+ mse = torch.mean(diff**2, dim=1)
168
+ n_eff = float(model.n_eff)
169
+ contribution[mask] = (
170
+ -0.5 * n_eff * mse / sigma**2
171
+ - n_eff * torch.log(sigma)
172
+ )
173
+ contributions[qoi] = contribution
151
174
 
152
- return log_likelihood
175
+ return contributions