BayesianEstimationTool 1.0.0__py3-none-any.whl
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.
- BayesianEstimationTool/BayesianEstimationTool.py +577 -0
- BayesianEstimationTool/BayesianEstimationTool_CheatSheet.py +245 -0
- BayesianEstimationTool/Method.py +10 -0
- BayesianEstimationTool/MethodValidation.py +61 -0
- BayesianEstimationTool/NutsMethod.py +57 -0
- BayesianEstimationTool/SviMethod.py +53 -0
- BayesianEstimationTool/__init__.py +6 -0
- bayesianestimationtool-1.0.0.dist-info/METADATA +68 -0
- bayesianestimationtool-1.0.0.dist-info/RECORD +12 -0
- bayesianestimationtool-1.0.0.dist-info/WHEEL +5 -0
- bayesianestimationtool-1.0.0.dist-info/licenses/LICENSE +201 -0
- bayesianestimationtool-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Generate BayesianEstimationTool_CheatSheet.pdf.
|
|
2
|
+
|
|
3
|
+
The cheat-sheet content is intentionally kept at the top of this file so this
|
|
4
|
+
source is useful as a readable reference even without generating the PDF.
|
|
5
|
+
|
|
6
|
+
Dependency:
|
|
7
|
+
pip install reportlab
|
|
8
|
+
|
|
9
|
+
Run:
|
|
10
|
+
python BayesianEstimationTool_CheatSheet.py
|
|
11
|
+
|
|
12
|
+
The PDF is written beside this file. The generator uses only ReportLab's
|
|
13
|
+
built-in Helvetica/Courier PDF fonts, so it does not depend on system fonts.
|
|
14
|
+
It automatically chooses the largest body font that keeps the cheat sheet on
|
|
15
|
+
one US Letter page.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# -----------------------------------------------------------------------------
|
|
20
|
+
# Cheat-sheet content
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
TITLE = "BayesianEstimationTool (BET) Cheat Sheet"
|
|
24
|
+
|
|
25
|
+
PURPOSE = (
|
|
26
|
+
"`Estimator` organizes hierarchical Bayesian estimation for datasets containing multiple related "
|
|
27
|
+
"entries. Models may contain global parameters shared across entries and standardized local Zs that vary by "
|
|
28
|
+
"entry. `Train` infers the population/global posterior, training local Zs, and local-Z correlation. `Test` carries "
|
|
29
|
+
"the learned population state to new entries and can either sample locals from the trained population or infer them from "
|
|
30
|
+
"new observations with a method. `ScoreTrainSamples` and `ScoreTestSamples` evaluate predictions. `Save` and `Load` preserve the "
|
|
31
|
+
"learned state and the settings needed to reconstruct how estimation was performed. Inference is supplied through an "
|
|
32
|
+
"Method interface; built-in methods include NUTS and SVI."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
DEPENDENCIES = "jax, numpyro, numpy"
|
|
36
|
+
|
|
37
|
+
INSTALLATION = (
|
|
38
|
+
"Install in editable mode with `python -m pip install -e \"/path/to/BayesianEstimationTool\"` then import with "
|
|
39
|
+
"`import BayesianEstimationTool as bet`. Editable installation means "
|
|
40
|
+
"repository code changes are used without reinstalling. For repository examples, the parent folder may instead be added "
|
|
41
|
+
"to `sys.path`."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
CONVENTIONS = [
|
|
45
|
+
("`import BayesianEstimationTool as bet`", "Public import; construct the main object with `bet.Estimator(...)` and supply inference through the `Method` argument."),
|
|
46
|
+
("`arg{}`", "dictionary whose values have no entry dimension; used for one entry's data, local Zs, model output, globals, or other named values."),
|
|
47
|
+
("`arg{[]}`", "dictionary containing arrays spanning multiple entries or posterior samples."),
|
|
48
|
+
("`arg[]`", "1D sequence."),
|
|
49
|
+
("`Fn(...) -> value`", "function argument or method together with the value it returns."),
|
|
50
|
+
("`globalDistributions{}`", "global-parameter prior dictionary: `{name: NumPyro distribution}`. BET creates the NumPyro sample sites internally; the dictionary keys become the global parameter names."),
|
|
51
|
+
("`likelihoods{}`", "single-entry likelihood dictionary: `{name: (observation, distribution)}`. BET vectorizes it across entries and creates the NumPyro observation sites internally."),
|
|
52
|
+
("*Italic arguments*", "accept `None` as a valid input."),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
FUNCTIONS = [
|
|
56
|
+
(
|
|
57
|
+
'Estimator(data{[]}, ModelFn(globalParams{}, localZs{}, data{}) -> modelOut{}, *localZNames[]*, *GlobalPriorFn(dataShapes{}) -> globalDistributions{}*, lkjConcentration=2.0, *rngSeed*) -> Estimator',
|
|
58
|
+
"Creates an Estimator. **localZNames** defines standardized entry-level parameters. With multiple locals, the Estimator learns their correlation with an LKJ Cholesky prior controlled by **lkjConcentration**. **GlobalPriorFn** receives the per-entry shapes of the full data and returns `{name: NumPyro distribution}`. BET creates the named NumPyro sample sites and passes their sampled values to **ModelFn** and likelihood functions as `globalParams{}`. **rngSeed** initializes the Estimator's persistent RNG.",
|
|
59
|
+
),
|
|
60
|
+
(
|
|
61
|
+
'Train(trainDataIdxs[], TrainLikelihoodFn(globalParams{}, localZs{}, data{}, modelOut{}) -> likelihoods{}, Method=method, printSummary=False, *rngSeed*, saveLocalZs=True, saveModelOut=False) -> methodOutput',
|
|
62
|
+
"Fits **trainDataIdxs** using **Method**. **TrainLikelihoodFn** is evaluated one entry at a time and returns `{name: (observation, NumPyro distribution)}`; BET batches the returned observations/distributions and creates the observation sites internally. A Method instance supplies an explicit configuration. A Method class reconstructs the saved training Method arguments; if its class name differs from the saved Method name, BET warns and still attempts reconstruction. The Estimator stores global posterior samples and the learned Cholesky state; **saveLocalZs** stores transformed training-local Z samples and **saveModelOut** stores model-output samples. **printSummary** uses the Estimator's generic posterior summary unless the Method replaces or augments it.",
|
|
63
|
+
),
|
|
64
|
+
(
|
|
65
|
+
'Test(testDataIdxs[], *TestLikelihoodFn(globalParams{}, localZs{}, data{}, modelOut{}) -> likelihoods{}*, *Method=method*, *nGlobalSamples*, nLocalZSamples=2000, printSummary=False, *rngSeed*, saveModelOut=False) -> None | methodOutput[]',
|
|
66
|
+
'Tests on new entries. **TestLikelihoodFn**, when supplied, follows the same single-entry `{name: (observation, distribution)}` contract as training. **nGlobalSamples=None** uses the median trained global/Cholesky state; a positive integer samples that many trained states. With **TestLikelihoodFn=None**, no Method is needed and the Estimator draws **nLocalZSamples** locals from the trained population. With a likelihood, a Method instance supplies an explicit configuration. A Method class reconstructs saved arguments: BET uses the previous Method-based test configuration when available, otherwise the training configuration. If the supplied class name differs from the saved Method name, BET warns and still attempts reconstruction.',
|
|
67
|
+
),
|
|
68
|
+
(
|
|
69
|
+
'ScoreTrainSamples(ScoreFn(modelOut{}, data{}) -> score{}, *nSamples*, *rngSeed*) -> scores{[]}',
|
|
70
|
+
'Scores training predictions. **nSamples=None** uses all available joint training-posterior samples; otherwise the Estimator selects that many. Saved model outputs are reused when available, otherwise they are regenerated from saved parameters.',
|
|
71
|
+
),
|
|
72
|
+
(
|
|
73
|
+
'ScoreTestSamples(ScoreFn(modelOut{}, data{}) -> score{}, *nSamples*, *rngSeed*) -> scores{[]}',
|
|
74
|
+
'Scores the latest test state. **nSamples=None** uses all stored local Z samples; otherwise that many are selected for each stored global state. Saved model outputs are reused when available, otherwise they are regenerated.',
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
'Save(*fileName*, saveTrainLocalZs=True, saveTestLocalZs=True, saveTrainModelOut=False, saveTestModelOut=False, saveAllData=False) -> None | record{}',
|
|
78
|
+
'Writes compressed `.npz` state, or returns a nested record when **fileName=None**. Saved metadata includes method class names and constructor arguments so the estimation setup can be reconstructed without serializing method machinery. Functions are not serialized.',
|
|
79
|
+
),
|
|
80
|
+
(
|
|
81
|
+
'Estimator.Load(fileName, *ModelFn*, *GlobalPriorFn*, *data*) -> Estimator | record{}',
|
|
82
|
+
'Loads saved state. With **ModelFn=None**, returns the saved record without reconstructing an Estimator. Otherwise model functions are supplied explicitly; **data** is required unless the data were saved with `saveAllData=True`.',
|
|
83
|
+
),
|
|
84
|
+
(
|
|
85
|
+
'Nuts(numWarmup=2000, numSamples=2000, numChains=4, acceptProb=0.95, denseMass=False, medianSamples=50) -> Nuts',
|
|
86
|
+
'Creates a NUTS inference Method, exposed as `bet.Nuts`. Pass `bet.Nuts(...)` for an explicit configuration or `bet.Nuts` to reconstruct saved Method arguments. **medianSamples** controls `init_to_median`; **acceptProb** is the target acceptance probability. NUTS retains posterior samples from the MCMC trajectory and prints NumPyro convergence diagnostics including n_eff and r_hat.',
|
|
87
|
+
),
|
|
88
|
+
(
|
|
89
|
+
'Svi(numSteps=10000, numSamples=2000, stepSize=0.01, numParticles=1, progressBar=True, medianSamples=50, multivariate=False) -> Svi',
|
|
90
|
+
'Creates an SVI inference Method, exposed as `bet.Svi`. Pass `bet.Svi(...)` for an explicit configuration or `bet.Svi` to reconstruct saved Method arguments. Uses `init_to_median`; **numSamples** is the number of posterior samples drawn from the fitted guide for the Estimator. By default the guide is mean-field `AutoNormal`; **multivariate=True** uses `AutoMultivariateNormal`, which can represent posterior covariance but scales less favorably for large latent spaces.',
|
|
91
|
+
),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# -----------------------------------------------------------------------------
|
|
96
|
+
# PDF generation
|
|
97
|
+
# -----------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
from pathlib import Path
|
|
100
|
+
import html
|
|
101
|
+
import re
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
from reportlab.lib import colors
|
|
105
|
+
from reportlab.lib.pagesizes import letter
|
|
106
|
+
from reportlab.lib.units import inch
|
|
107
|
+
from reportlab.platypus import BaseDocTemplate, Frame, KeepTogether, PageTemplate, Paragraph
|
|
108
|
+
from reportlab.lib.styles import ParagraphStyle
|
|
109
|
+
except ImportError as exc:
|
|
110
|
+
raise SystemExit(
|
|
111
|
+
"Generating the PDF requires ReportLab. Install it with: pip install reportlab"
|
|
112
|
+
) from exc
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
OUTPUT_FILE = Path(__file__).with_name("BayesianEstimationTool_CheatSheet.pdf")
|
|
116
|
+
MIN_BODY_FONT = 7.0
|
|
117
|
+
MAX_BODY_FONT = 11.5
|
|
118
|
+
FONT_STEP = 0.1
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _markup(text):
|
|
122
|
+
"""Convert the tiny readable markup used above to ReportLab paragraph markup."""
|
|
123
|
+
text = html.escape(text, quote=False).replace("->", "->")
|
|
124
|
+
text = re.sub(r"`([^`]+)`", r"<font name='Courier'>\1</font>", text)
|
|
125
|
+
text = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", text)
|
|
126
|
+
text = re.sub(r"\*([^*]+)\*", r"<i>\1</i>", text)
|
|
127
|
+
return text
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _signature_markup(text):
|
|
131
|
+
"""Render signatures bold overall, with None-accepting arguments italicized."""
|
|
132
|
+
text = html.escape(text, quote=False).replace("->", "->")
|
|
133
|
+
text = re.sub(r"\*([^*]+)\*", r"<i>\1</i>", text)
|
|
134
|
+
return text
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _build_pdf(body_font):
|
|
138
|
+
"""Build the PDF at one candidate font size and return its page count."""
|
|
139
|
+
page_width, page_height = letter
|
|
140
|
+
margin = 0.38 * inch
|
|
141
|
+
header_height = 0.38 * inch
|
|
142
|
+
usable_height = page_height - 2 * margin - header_height
|
|
143
|
+
|
|
144
|
+
frame = Frame(
|
|
145
|
+
margin,
|
|
146
|
+
margin,
|
|
147
|
+
page_width - 2 * margin,
|
|
148
|
+
usable_height,
|
|
149
|
+
leftPadding=4,
|
|
150
|
+
rightPadding=4,
|
|
151
|
+
topPadding=2,
|
|
152
|
+
bottomPadding=2,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def draw_header(canvas, doc):
|
|
156
|
+
canvas.saveState()
|
|
157
|
+
canvas.setFont("Helvetica-Bold", 16)
|
|
158
|
+
canvas.drawString(margin, page_height - margin - 10, TITLE)
|
|
159
|
+
canvas.setStrokeColor(colors.HexColor("#888888"))
|
|
160
|
+
canvas.setLineWidth(0.5)
|
|
161
|
+
canvas.line(margin, page_height - margin - 16, page_width - margin, page_height - margin - 16)
|
|
162
|
+
canvas.restoreState()
|
|
163
|
+
|
|
164
|
+
purpose_style = ParagraphStyle(
|
|
165
|
+
"Purpose",
|
|
166
|
+
fontName="Helvetica",
|
|
167
|
+
fontSize=body_font + 0.8,
|
|
168
|
+
leading=(body_font + 0.8) * 1.18,
|
|
169
|
+
spaceAfter=4,
|
|
170
|
+
)
|
|
171
|
+
heading_style = ParagraphStyle(
|
|
172
|
+
"Heading",
|
|
173
|
+
fontName="Helvetica-Bold",
|
|
174
|
+
fontSize=body_font + 2.5,
|
|
175
|
+
leading=(body_font + 2.5) * 1.12,
|
|
176
|
+
spaceBefore=2.5,
|
|
177
|
+
spaceAfter=4.0,
|
|
178
|
+
)
|
|
179
|
+
conventions_style = ParagraphStyle(
|
|
180
|
+
"Conventions",
|
|
181
|
+
fontName="Helvetica",
|
|
182
|
+
fontSize=body_font,
|
|
183
|
+
leading=body_font * 1.18,
|
|
184
|
+
spaceAfter=2.5,
|
|
185
|
+
backColor=colors.HexColor("#F4F4F4"),
|
|
186
|
+
borderPadding=4,
|
|
187
|
+
)
|
|
188
|
+
entry_style = ParagraphStyle(
|
|
189
|
+
"Entry",
|
|
190
|
+
fontName="Helvetica",
|
|
191
|
+
fontSize=body_font,
|
|
192
|
+
leading=body_font * 1.17,
|
|
193
|
+
spaceBefore=2.2,
|
|
194
|
+
spaceAfter=1.6,
|
|
195
|
+
leftIndent=14,
|
|
196
|
+
firstLineIndent=-14,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
convention_lines = [f"<b>{_markup(symbol)}</b> - {_markup(description)}" for symbol, description in CONVENTIONS]
|
|
200
|
+
|
|
201
|
+
story = [
|
|
202
|
+
Paragraph("<b>Purpose:</b> " + _markup(PURPOSE) + "<br/><b>Dependencies:</b> " + _markup(DEPENDENCIES) + "<br/><b>Installation:</b> " + _markup(INSTALLATION), purpose_style),
|
|
203
|
+
Paragraph("Conventions", heading_style),
|
|
204
|
+
Paragraph("<br/>".join(convention_lines), conventions_style),
|
|
205
|
+
Paragraph("Main Functions", heading_style),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
for signature, description in FUNCTIONS:
|
|
209
|
+
story.append(
|
|
210
|
+
KeepTogether([
|
|
211
|
+
Paragraph(f"<b>{_signature_markup(signature)}:</b> {_markup(description)}", entry_style)
|
|
212
|
+
])
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
doc = BaseDocTemplate(
|
|
216
|
+
str(OUTPUT_FILE),
|
|
217
|
+
pagesize=letter,
|
|
218
|
+
leftMargin=margin,
|
|
219
|
+
rightMargin=margin,
|
|
220
|
+
topMargin=margin,
|
|
221
|
+
bottomMargin=margin,
|
|
222
|
+
)
|
|
223
|
+
doc.addPageTemplates(PageTemplate(id="Main", frames=[frame], onPage=draw_header))
|
|
224
|
+
doc.build(story)
|
|
225
|
+
return doc.page
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def generate_pdf():
|
|
229
|
+
"""Use the largest body font that keeps the cheat sheet on one page."""
|
|
230
|
+
steps = round((MAX_BODY_FONT - MIN_BODY_FONT) / FONT_STEP)
|
|
231
|
+
candidate_sizes = [round(MAX_BODY_FONT - i * FONT_STEP, 1) for i in range(steps + 1)]
|
|
232
|
+
|
|
233
|
+
for body_font in candidate_sizes:
|
|
234
|
+
if _build_pdf(body_font) == 1:
|
|
235
|
+
print(f"Wrote {OUTPUT_FILE.name} at {body_font:.1f} pt body font.")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
raise RuntimeError(
|
|
239
|
+
f"Cheat sheet does not fit on one page even at {MIN_BODY_FONT:.1f} pt. "
|
|
240
|
+
"Shorten the content or lower MIN_BODY_FONT."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
generate_pdf()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Structural validation of BET inference Method instances.
|
|
2
|
+
|
|
3
|
+
This checks interfaces and saved configuration only; it does not run inference.
|
|
4
|
+
"""
|
|
5
|
+
import inspect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def ValidateMethod(method):
|
|
9
|
+
"""Validate a configured BET Method; raise a specific error or return True.
|
|
10
|
+
|
|
11
|
+
_Fit must accept (ModelFn, rngKey, samplePrefixes) positionally and
|
|
12
|
+
return {"output": ..., "samples": {site_name: sample_array, ...}}
|
|
13
|
+
when executed. Runtime return values are deliberately not inspected here.
|
|
14
|
+
"""
|
|
15
|
+
if method is None or inspect.isclass(method):
|
|
16
|
+
raise TypeError("Method must be a configured instance, not None or a class.")
|
|
17
|
+
from .Method import Method as BaseMethod
|
|
18
|
+
for name in ("_Fit", "_SaveArgs"):
|
|
19
|
+
if getattr(type(method), name, None) is getattr(BaseMethod, name):
|
|
20
|
+
raise TypeError(f"Method must override inherited {name}(); the base implementation raises NotImplementedError.")
|
|
21
|
+
for name, nargs in (("_Fit", 3), ("_SaveArgs", 0)):
|
|
22
|
+
fn = getattr(method, name, None)
|
|
23
|
+
if not callable(fn):
|
|
24
|
+
raise TypeError(f"Method must implement callable {name}().")
|
|
25
|
+
try:
|
|
26
|
+
signature = inspect.signature(fn)
|
|
27
|
+
except (TypeError, ValueError):
|
|
28
|
+
continue
|
|
29
|
+
try:
|
|
30
|
+
signature.bind(*([None] * nargs))
|
|
31
|
+
except TypeError as exc:
|
|
32
|
+
raise TypeError(f"Method.{name} must accept {nargs} positional arguments; got {signature}.") from exc
|
|
33
|
+
mode = getattr(method, "_printMode", None)
|
|
34
|
+
if mode not in (None, "before", "after", "replace"):
|
|
35
|
+
raise ValueError("Method._printMode must be None, 'before', 'after', or 'replace'.")
|
|
36
|
+
if mode is not None:
|
|
37
|
+
fn = getattr(method, "_Print", None)
|
|
38
|
+
if not callable(fn):
|
|
39
|
+
raise TypeError("Method._Print must be callable when _printMode is set.")
|
|
40
|
+
try:
|
|
41
|
+
signature = inspect.signature(fn)
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
pass
|
|
44
|
+
else:
|
|
45
|
+
try:
|
|
46
|
+
signature.bind(None, None, None, None)
|
|
47
|
+
except TypeError as exc:
|
|
48
|
+
raise TypeError(f"Method._Print must accept output, localNames, globalNames, title; got {signature}.") from exc
|
|
49
|
+
try:
|
|
50
|
+
args = method._SaveArgs()
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
raise ValueError("Method._SaveArgs() raised an exception.") from exc
|
|
53
|
+
if not isinstance(args, dict):
|
|
54
|
+
raise TypeError("Method._SaveArgs() must return a dictionary.")
|
|
55
|
+
if any(not isinstance(key, str) for key in args):
|
|
56
|
+
raise TypeError("Method._SaveArgs() keys must be strings.")
|
|
57
|
+
try:
|
|
58
|
+
inspect.signature(type(method)).bind(**args)
|
|
59
|
+
except (TypeError, ValueError) as exc:
|
|
60
|
+
raise TypeError("Method._SaveArgs() keys must be accepted by the Method constructor.") from exc
|
|
61
|
+
return True
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from .Method import Method
|
|
2
|
+
from numpyro.infer import init_to_median,NUTS,MCMC
|
|
3
|
+
from numpyro.diagnostics import summary
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Nuts(Method):
|
|
8
|
+
_printMode="replace"
|
|
9
|
+
|
|
10
|
+
def __init__(self,numWarmup=2000,numSamples=2000,numChains=4,acceptProb=0.95,denseMass=False,medianSamples=50):
|
|
11
|
+
if isinstance(numWarmup,(bool,np.bool_)) or not isinstance(numWarmup,(int,np.integer)): raise TypeError(f"numWarmup must be an integer; got {type(numWarmup).__name__}.")
|
|
12
|
+
if numWarmup<0: raise ValueError("numWarmup must be >= 0.")
|
|
13
|
+
for value,name in ((numSamples,"numSamples"),(numChains,"numChains"),(medianSamples,"medianSamples")):
|
|
14
|
+
if isinstance(value,(bool,np.bool_)) or not isinstance(value,(int,np.integer)): raise TypeError(f"{name} must be an integer; got {type(value).__name__}.")
|
|
15
|
+
if value<=0: raise ValueError(f"{name} must be > 0.")
|
|
16
|
+
if isinstance(acceptProb,(bool,np.bool_)) or not isinstance(acceptProb,(int,float,np.integer,np.floating)): raise TypeError(f"acceptProb must be a real number; got {type(acceptProb).__name__}.")
|
|
17
|
+
if not np.isfinite(acceptProb): raise ValueError("acceptProb must be finite.")
|
|
18
|
+
if not 0<acceptProb<1: raise ValueError("acceptProb must be between 0 and 1.")
|
|
19
|
+
if not isinstance(denseMass,(bool,np.bool_)): raise TypeError(f"denseMass must be bool; got {type(denseMass).__name__}.")
|
|
20
|
+
self.numWarmup=numWarmup
|
|
21
|
+
self.numSamples=numSamples
|
|
22
|
+
self.numChains=numChains
|
|
23
|
+
self.acceptProb=acceptProb
|
|
24
|
+
self.denseMass=denseMass
|
|
25
|
+
self.medianSamples=medianSamples
|
|
26
|
+
|
|
27
|
+
def _Fit(self,ModelFn,rngKey,samplePrefixes):
|
|
28
|
+
kernel=NUTS(ModelFn,init_strategy=init_to_median(num_samples=self.medianSamples),target_accept_prob=self.acceptProb,dense_mass=self.denseMass)
|
|
29
|
+
mcmc=MCMC(kernel,num_warmup=self.numWarmup,num_samples=self.numSamples,num_chains=self.numChains,chain_method="parallel")
|
|
30
|
+
mcmc.run(rngKey,extra_fields=("num_steps","accept_prob"))
|
|
31
|
+
post=mcmc.get_samples(group_by_chain=False)
|
|
32
|
+
samples={name:value for name,value in post.items() if any(name.startswith(prefix) for prefix in samplePrefixes)}
|
|
33
|
+
return {"output":mcmc,"samples":samples}
|
|
34
|
+
|
|
35
|
+
def _Print(self,output,localNames,globalNames,title=None):
|
|
36
|
+
stats=summary(output.get_samples(group_by_chain=True),prob=0.95)
|
|
37
|
+
localNames=list(localNames)
|
|
38
|
+
globalNames=[] if globalNames is None else list(globalNames)
|
|
39
|
+
if title is not None: print(title)
|
|
40
|
+
print(f"{'parameter':<20} {'mean':>10} {'std':>10} {'median':>10} {'2.5%':>10} {'97.5%':>10} {'n_eff':>10} {'r_hat':>8}")
|
|
41
|
+
for prefix,names in (("__globalParam__",globalNames),("__localZ__",localNames)):
|
|
42
|
+
for name in names:
|
|
43
|
+
statName=f"{prefix}{name}"
|
|
44
|
+
if statName not in stats: continue
|
|
45
|
+
shape=np.shape(stats[statName]["mean"])
|
|
46
|
+
if not shape: self._PrintRow(name,stats[statName])
|
|
47
|
+
else:
|
|
48
|
+
for inds in np.ndindex(shape): self._PrintRow(f"{name}[{','.join(map(str,inds))}]",stats[statName],inds)
|
|
49
|
+
|
|
50
|
+
def _SaveArgs(self):
|
|
51
|
+
return {"numWarmup":self.numWarmup,"numSamples":self.numSamples,"numChains":self.numChains,"acceptProb":self.acceptProb,"denseMass":self.denseMass,"medianSamples":self.medianSamples}
|
|
52
|
+
|
|
53
|
+
def _PrintRow(self,label,stats,inds=()):
|
|
54
|
+
def get(key):
|
|
55
|
+
x=np.asarray(stats[key])
|
|
56
|
+
return x[inds] if inds else x.item()
|
|
57
|
+
print(f"{label:<20} {get('mean'):>10.3f} {get('std'):>10.3f} {get('median'):>10.3f} {get('2.5%'):>10.3f} {get('97.5%'):>10.3f} {get('n_eff'):>10.1f} {get('r_hat'):>8.2f}")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from .Method import Method
|
|
2
|
+
from jax import random
|
|
3
|
+
from numpyro.infer import SVI as NumPyroSVI,Trace_ELBO,Predictive
|
|
4
|
+
from numpyro.infer.autoguide import AutoNormal,AutoMultivariateNormal
|
|
5
|
+
from numpyro.optim import Adam
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpyro.infer import init_to_median
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Svi(Method):
|
|
11
|
+
_printMode="after"
|
|
12
|
+
def __init__(self,numSteps=10000,numSamples=2000,stepSize=0.01,numParticles=1,progressBar=True,medianSamples=50,multivariate=False):
|
|
13
|
+
for value,name in ((numSteps,"numSteps"),(numSamples,"numSamples"),(numParticles,"numParticles"),(medianSamples,"medianSamples")):
|
|
14
|
+
if isinstance(value,(bool,np.bool_)) or not isinstance(value,(int,np.integer)): raise TypeError(f"{name} must be an integer; got {type(value).__name__}.")
|
|
15
|
+
if value<=0: raise ValueError(f"{name} must be > 0.")
|
|
16
|
+
if not isinstance(progressBar,(bool,np.bool_)): raise TypeError(f"progressBar must be bool; got {type(progressBar).__name__}.")
|
|
17
|
+
if not isinstance(multivariate,(bool,np.bool_)): raise TypeError(f"multivariate must be bool; got {type(multivariate).__name__}.")
|
|
18
|
+
if isinstance(stepSize,(bool,np.bool_)) or not isinstance(stepSize,(int,float,np.integer,np.floating)):
|
|
19
|
+
raise TypeError(f"stepSize must be a real number; got {type(stepSize).__name__}.")
|
|
20
|
+
if not np.isfinite(stepSize) or stepSize<=0: raise ValueError("stepSize must be finite and > 0.")
|
|
21
|
+
self.numSteps=numSteps
|
|
22
|
+
self.numSamples=numSamples
|
|
23
|
+
self.stepSize=stepSize
|
|
24
|
+
self.numParticles=numParticles
|
|
25
|
+
self.progressBar=progressBar
|
|
26
|
+
self.medianSamples=medianSamples
|
|
27
|
+
self.multivariate=multivariate
|
|
28
|
+
self._params={}
|
|
29
|
+
|
|
30
|
+
def _Fit(self,ModelFn,rngKey,samplePrefixes):
|
|
31
|
+
fitKey,sampleKey=random.split(rngKey)
|
|
32
|
+
Guide=AutoMultivariateNormal if self.multivariate else AutoNormal
|
|
33
|
+
guide=Guide(ModelFn,init_loc_fn=init_to_median(num_samples=self.medianSamples))
|
|
34
|
+
svi=NumPyroSVI(ModelFn,guide,Adam(self.stepSize),Trace_ELBO(num_particles=self.numParticles))
|
|
35
|
+
output=svi.run(fitKey,self.numSteps,progress_bar=self.progressBar)
|
|
36
|
+
self._params=output.params
|
|
37
|
+
predictive=Predictive(ModelFn,guide=guide,params=self._params,num_samples=self.numSamples)
|
|
38
|
+
post=predictive(sampleKey)
|
|
39
|
+
samples={name:value for name,value in post.items() if any(name.startswith(prefix) for prefix in samplePrefixes)}
|
|
40
|
+
return {"output":output,"samples":samples}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _Print(self,output,localNames,globalNames,title=None):
|
|
44
|
+
if title is not None: print(title)
|
|
45
|
+
losses=np.asarray(output.losses)
|
|
46
|
+
print(f"SVI steps: {len(losses)}")
|
|
47
|
+
if len(losses)>0:
|
|
48
|
+
print(f"Initial loss: {losses[0]:.6g}")
|
|
49
|
+
print(f"Final loss: {losses[-1]:.6g}")
|
|
50
|
+
|
|
51
|
+
def _SaveArgs(self):
|
|
52
|
+
return {"numSteps":self.numSteps,"numSamples":self.numSamples,"stepSize":self.stepSize,"numParticles":self.numParticles,"progressBar":self.progressBar,"medianSamples":self.medianSamples,"multivariate":self.multivariate}
|
|
53
|
+
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: BayesianEstimationTool
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Hierarchical Bayesian estimation and prediction for mechanistic models
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Project-URL: Repository, https://github.com/rafaelrbravo/BayesianEstimationTool
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: jax
|
|
11
|
+
Requires-Dist: numpyro
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: cloudpickle
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# BayesianEstimationTool (BET)
|
|
17
|
+
|
|
18
|
+
BayesianEstimationTool fits population-level parameters and individual-specific latent variables with NumPyro inference methods.
|
|
19
|
+
|
|
20
|
+
## Documentation
|
|
21
|
+
|
|
22
|
+
Start here:
|
|
23
|
+
|
|
24
|
+
- **[Manual (Markdown)](docs/Manual.md)** — model contracts, likelihoods, masking, JAX modeling, inference, persistence, and testing.
|
|
25
|
+
- **[Manual (PDF)](docs/Manual.pdf)** — printable manual. If GitHub cannot preview the PDF, download it to view locally.
|
|
26
|
+
- **[Cheatsheet (Markdown)](docs/Cheatsheet.md)** — compact API reference and common workflows.
|
|
27
|
+
- **[Cheatsheet (PDF)](docs/Cheatsheet.pdf)** — one-page printable reference. If GitHub cannot preview the PDF, download it to view locally.
|
|
28
|
+
|
|
29
|
+
**Manual.md is the authoritative manual.** Its PDF is rendered directly from that file using GitHub-like styling and print pagination. The cheatsheet is a separate compact, Python-generated reference. To regenerate locally, install `requests`, `playwright`, and `reportlab`, install Playwright Chromium (`python -m playwright install chromium`), and set `GITHUB_TOKEN` to a token permitted to call GitHub's Markdown API for this repository. The manual generator uses GitHub's Markdown rendering API and Playwright; the cheatsheet generator uses ReportLab:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python docs/generators/markdown_to_pdf.py docs/Manual.md
|
|
33
|
+
python docs/generators/cheatsheet.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The GitHub Actions test workflow regenerates the PDFs (and the Python-authored cheatsheet Markdown), checks that the cheatsheet is exactly one PDF page, uploads the outputs as a `bet-docs` artifact, and runs the test suite.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
BET requires Python 3.10 or newer. **After the first release is published**, install the latest version from [PyPI](https://pypi.org/project/BayesianEstimationTool/):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python -m pip install BayesianEstimationTool
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
To install the specific first release:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python -m pip install BayesianEstimationTool==1.0.0
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Alternatively, install directly from the tagged GitHub source:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m pip install "git+https://github.com/rafaelrbravo/BayesianEstimationTool.git@v1.0.0"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You can also download a wheel (`.whl`) or source archive (`.tar.gz`) from [GitHub Releases](https://github.com/rafaelrbravo/BayesianEstimationTool/releases) and install it with `python -m pip install PATH_TO_DOWNLOADED_FILE`. These release assets become available after publishing.
|
|
59
|
+
|
|
60
|
+
Verify installation:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python -c "import BayesianEstimationTool as bet; print(bet.Estimator)"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For editable development from a source checkout, run `python -m pip install -e .` in the repository root. The conda recipe in `conda-recipe/meta.yaml` is for local builds; BET is not yet published on conda-forge.
|
|
67
|
+
|
|
68
|
+
See the manual for the minimal model and training examples.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
BayesianEstimationTool/BayesianEstimationTool.py,sha256=yG7BLFbnLeU66kjMwI-46zcvlyimTd6pKK8rQRYAzHw,36485
|
|
2
|
+
BayesianEstimationTool/BayesianEstimationTool_CheatSheet.py,sha256=HZPvU0UfX9RmDBVc01cWZY-OPMPiTAnsf7geVBmh9MY,13364
|
|
3
|
+
BayesianEstimationTool/Method.py,sha256=zn7ju_9nILgnpwfbHUAsTidRfhjmLDS2jdKIqwoEbi0,196
|
|
4
|
+
BayesianEstimationTool/MethodValidation.py,sha256=RH6Srn9UueJV8SAnQg8Bgs1ycAdAlq2FgftIYYofHHQ,2796
|
|
5
|
+
BayesianEstimationTool/NutsMethod.py,sha256=g6nI_WsZbpSnu1G_8m7Da6bmJw0ckDVJJagUVWV1tgY,3678
|
|
6
|
+
BayesianEstimationTool/SviMethod.py,sha256=M76zQo56nsGyrnQS4DDrh8ftiAQMnRacjLJt2kvfogk,3050
|
|
7
|
+
BayesianEstimationTool/__init__.py,sha256=7p9sf6KXsM-P8JrsWto0Vtx9tCm2QGHJLU3eyNVWg_4,200
|
|
8
|
+
bayesianestimationtool-1.0.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
9
|
+
bayesianestimationtool-1.0.0.dist-info/METADATA,sha256=74OYZ9VylmD73kByKSViM6A5sJqp3YQnJkpTYoBb0C0,3221
|
|
10
|
+
bayesianestimationtool-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
11
|
+
bayesianestimationtool-1.0.0.dist-info/top_level.txt,sha256=ZlzIcp6AOhAeZRPEGQRe1WQRgdLEK7w4LtDZZzMWS5w,23
|
|
12
|
+
bayesianestimationtool-1.0.0.dist-info/RECORD,,
|