Skip to content

Commit 45768fe

Browse files
committed
merge upstream/dev into doc-autosummary
2 parents 2367a21 + fc86d4d commit 45768fe

File tree

107 files changed

+4544
-1707
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

107 files changed

+4544
-1707
lines changed

.github/workflows/tests.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ jobs:
4646
test:
4747
runs-on: ${{ matrix.os }}
4848
strategy:
49+
fail-fast: false
4950
matrix:
5051
os: [ubuntu-latest, windows-latest]
5152
python-version: ["3.10", "3.11"]

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@ fueled by continuous progress in generative AI and Bayesian inference.
1616

1717
## Conceptual Overview
1818

19+
<div align="center">
20+
<picture>
21+
<source media="(prefers-color-scheme: dark)" srcset="./img/bayesflow_landing_dark.jpg">
22+
<source media="(prefers-color-scheme: light)" srcset="./img/bayesflow_landing_light.jpg">
23+
<img alt="dsd" src="./img/bayesflow_landing_dark.jpg">
24+
</picture>
25+
</div>
26+
1927
A cornerstone idea of amortized Bayesian inference is to employ generative
2028
neural networks for parameter estimation, model comparison, and model validation
2129
when working with intractable simulators whose behavior as a whole is too
22-
complex to be described analytically. The figure below presents a higher-level
23-
overview of neurally bootstrapped Bayesian inference.
24-
25-
<img src="https://github.com/bayesflow-org/bayesflow/blob/master/img/high_level_framework.png?raw=true" width=80% height=80%>
26-
30+
complex to be described analytically.
2731

2832
## Disclaimer
2933

@@ -91,9 +95,10 @@ Check out some of our walk-through notebooks below. We are actively working on p
9195

9296
1. [Two moons starter toy example](examples/TwoMoons_StarterNotebook.ipynb)
9397
2. [Linear regression](examples/Linear_Regression.ipynb)
94-
3. [Hyperparameter optimization](examples/Hyperparameter_Optimization.ipynb)
95-
4. [Bayesian experimental design](examples/Bayesian_Experimental_Design.ipynb)
96-
5. Coming soon...
98+
3. [Bayesian experimental design](examples/Bayesian_Experimental_Design.ipynb)
99+
4. [SIR model with custom summary network](examples/SIR_PosteriorEstimation.ipynb)
100+
5. [Hyperparameter optimization](examples/Hyperparameter_Optimization.ipynb)
101+
6. Coming soon...
97102

98103
## Documentation \& Help
99104

bayesflow/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
distributions,
88
networks,
99
simulators,
10+
workflows,
1011
utils,
1112
)
1213

14+
from .workflows import BasicWorkflow
1315
from .approximators import ContinuousApproximator
1416
from .adapters import Adapter
1517
from .datasets import OfflineDataset, OnlineDataset, DiskDataset

bayesflow/adapters/adapter.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99

1010
from .transforms import (
1111
AsSet,
12+
AsTimeSeries,
1213
Broadcast,
1314
Concatenate,
1415
Constrain,
1516
ConvertDType,
1617
Drop,
18+
ExpandDims,
1719
FilterTransform,
1820
Keep,
1921
LambdaTransform,
@@ -111,24 +113,33 @@ def as_set(self, keys: str | Sequence[str]):
111113
self.transforms.append(transform)
112114
return self
113115

114-
def broadcast(self, keys: str | Sequence[str], *, expand_scalars: bool = True):
116+
def as_time_series(self, keys: str | Sequence[str]):
115117
if isinstance(keys, str):
116118
keys = [keys]
117119

118-
transform = MapTransform({key: Broadcast(expand_scalars=expand_scalars) for key in keys})
120+
transform = MapTransform({key: AsTimeSeries() for key in keys})
121+
self.transforms.append(transform)
122+
return self
123+
124+
def broadcast(
125+
self, keys: str | Sequence[str], *, to: str, expand: str | int | tuple = "left", exclude: int | tuple = -1
126+
):
127+
if isinstance(keys, str):
128+
keys = [keys]
129+
130+
transform = Broadcast(keys, to=to, expand=expand, exclude=exclude)
119131
self.transforms.append(transform)
120132
return self
121133

122134
def clear(self):
123135
self.transforms = []
124136
return self
125137

126-
def concatenate(self, keys: Sequence[str], *, into: str, axis: int = -1):
138+
def concatenate(self, keys: str | Sequence[str], *, into: str, axis: int = -1):
127139
if isinstance(keys, str):
128-
# this is a common mistake, and also passes the type checker since str is a sequence of characters
129-
raise ValueError("Keys must be a sequence of strings. To rename a single key, use the `rename` method.")
130-
131-
transform = Concatenate(keys, into=into, axis=axis)
140+
transform = Rename(keys, to_key=into)
141+
else:
142+
transform = Concatenate(keys, into=into, axis=axis)
132143
self.transforms.append(transform)
133144
return self
134145

@@ -177,6 +188,14 @@ def drop(self, keys: str | Sequence[str]):
177188
self.transforms.append(transform)
178189
return self
179190

191+
def expand_dims(self, keys: str | Sequence[str], *, axis: int | tuple):
192+
if isinstance(keys, str):
193+
keys = [keys]
194+
195+
transform = ExpandDims(keys, axis=axis)
196+
self.transforms.append(transform)
197+
return self
198+
180199
def keep(self, keys: str | Sequence[str]):
181200
if isinstance(keys, str):
182201
keys = [keys]

bayesflow/adapters/transforms/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from .as_set import AsSet
2+
from .as_time_series import AsTimeSeries
23
from .broadcast import Broadcast
34
from .concatenate import Concatenate
45
from .constrain import Constrain
56
from .convert_dtype import ConvertDType
67
from .drop import Drop
78
from .elementwise_transform import ElementwiseTransform
9+
from .expand_dims import ExpandDims
810
from .filter_transform import FilterTransform
911
from .keep import Keep
1012
from .lambda_transform import LambdaTransform

bayesflow/adapters/transforms/as_set.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@
44

55

66
class AsSet(ElementwiseTransform):
7+
"""
8+
The `.as_set(["x", "y"])` transform indicates that both `x` and `y` are treated as sets.
9+
That is, their values will be treated as *exchangable* such that they will imply
10+
the same inference regardless of the values' order.
11+
This is useful, for example, in a linear regression context where we can index
12+
the observations in arbitrary order and always get the same regression line.
13+
14+
Currently, all this transform does is to ensure that the variable
15+
arrays are at least 3D. The 2rd dimension is treated as the
16+
set dimension and the 3rd dimension as the data dimension.
17+
In the future, the transform will have more advanced behavior
18+
to better ensure the correct treatment of sets.
19+
20+
Useage:
21+
22+
adapter = (
23+
bf.Adapter()
24+
.as_set(["x", "y"])
25+
)
26+
"""
27+
728
def forward(self, data: np.ndarray, **kwargs) -> np.ndarray:
829
return np.atleast_3d(data)
930

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import numpy as np
2+
3+
from .elementwise_transform import ElementwiseTransform
4+
5+
6+
class AsTimeSeries(ElementwiseTransform):
7+
"""
8+
The `.as_time_series` transform can be used to indicate that
9+
variables shall be treated as time series.
10+
11+
Currently, all this transformation does is to ensure that the variable
12+
arrays are at least 3D. The 2rd dimension is treated as the
13+
time series dimension and the 3rd dimension as the data dimension.
14+
In the future, the transform will have more advanced behavior
15+
to better ensure the correct treatment of time series data.
16+
17+
Useage:
18+
19+
adapter = (
20+
bf.Adapter()
21+
.as_time_series(["x", "y"])
22+
)
23+
"""
24+
25+
def forward(self, data: np.ndarray, **kwargs) -> np.ndarray:
26+
return np.atleast_3d(data)
27+
28+
def inverse(self, data: np.ndarray, **kwargs) -> np.ndarray:
29+
if data.shape[2] == 1:
30+
return np.squeeze(data, axis=2)
31+
32+
return data
Lines changed: 105 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,125 @@
1+
from collections.abc import Sequence
12
import numpy as np
23

3-
from .elementwise_transform import ElementwiseTransform
4+
from keras.saving import (
5+
deserialize_keras_object as deserialize,
6+
register_keras_serializable as serializable,
7+
serialize_keras_object as serialize,
8+
)
49

10+
from .transform import Transform
511

6-
class Broadcast(ElementwiseTransform):
12+
13+
@serializable(package="bayesflow.adapters")
14+
class Broadcast(Transform):
715
"""
8-
Broadcasts array to a given batch size.
16+
Broadcasts arrays or scalars to the shape of a given other array.
17+
18+
Parameters:
19+
20+
expand: Where should new dimensions be added to match the number of dimensions in `to`?
21+
Can be "left", "right", or an integer or tuple containing the indices of the new dimensions.
22+
The latter is needed if we want to include a dimension in the middle, which will be required
23+
for more advanced cases. By default we expand left.
24+
25+
exclude: Which dimensions (of the dimensions after expansion) should retain their size,
26+
rather than being broadcasted to the corresponding dimension size of `to`?
27+
By default we exclude the last dimension (usually the data dimension) from broadcasting the size.
28+
929
Examples:
10-
>>> bc = Broadcast()
11-
>>> bc(np.array(5), batch_size=3)
12-
array([[5], [5], [5]])
13-
>>> bc(np.array(5), batch_size=3).shape
14-
(3, 1)
15-
>>> bc(np.array([1, 2, 3]), batch_size=3)
16-
array([[1, 2, 3], [1, 2, 3], [1, 2, 3]])
17-
>>> bc(np.array([1, 2, 3]), batch_size=3).shape
18-
(3, 3)
19-
20-
You can opt out of expanding scalars:
21-
>>> bc = Broadcast(expand_scalars=False)
22-
>>> bc(np.array(5), batch_size=3)
23-
np.array([5, 5, 5])
24-
>>> bc(np.array(5), batch_size=3).shape
25-
(3,)
30+
shape (1, ) array:
31+
>>> a = np.array((1,))
32+
shape (2, 3) array:
33+
>>> b = np.array([[1, 2, 3], [4, 5, 6]])
34+
shape (2, 2, 3) array:
35+
>>> c = np.array([[[1, 2, 3], [4, 5, 6]], [[4, 5, 6], [1, 2, 3]]])
36+
>>> dat = dict(a=a, b=b, c=c)
37+
38+
>>> bc = bf.adapters.transforms.Broadcast("a", to="b")
39+
>>> new_dat = bc.forward(dat)
40+
>>> new_dat["a"].shape
41+
(2, 1)
42+
43+
>>> bc = bf.adapters.transforms.Broadcast("a", to="b", exclude=None)
44+
>>> new_dat = bc.forward(dat)
45+
>>> new_dat["a"].shape
46+
(2, 3)
47+
48+
>>> bc = bf.adapters.transforms.Broadcast("b", to="c", expand=1)
49+
>>> new_dat = bc.forward(dat)
50+
>>> new_dat["b"].shape
51+
(2, 2, 3)
2652
2753
It is recommended to precede this transform with a :class:`bayesflow.adapters.transforms.ToArray` transform.
2854
"""
2955

30-
def __init__(self, *, expand_scalars: bool = True):
56+
def __init__(self, keys: Sequence[str], *, to: str, expand: str | int | tuple = "left", exclude: int | tuple = -1):
3157
super().__init__()
58+
self.keys = keys
59+
self.to = to
3260

33-
self.expand_scalars = expand_scalars
61+
if isinstance(expand, int):
62+
expand = (expand,)
3463

35-
# noinspection PyMethodOverriding
36-
def forward(self, data: np.ndarray, *, batch_size: int, **kwargs):
37-
data = np.repeat(data[None], batch_size, axis=0)
64+
self.expand = expand
3865

39-
if self.expand_scalars and data.ndim == 1:
40-
data = data[:, None]
66+
if isinstance(exclude, int):
67+
exclude = (exclude,)
4168

42-
return data
69+
self.exclude = exclude
70+
71+
@classmethod
72+
def from_config(cls, config: dict, custom_objects=None) -> "Broadcast":
73+
return cls(
74+
keys=deserialize(config["keys"], custom_objects),
75+
to=deserialize(config["to"], custom_objects),
76+
expand=deserialize(config["expand"], custom_objects),
77+
exclude=deserialize(config["exclude"], custom_objects),
78+
)
79+
80+
def get_config(self) -> dict:
81+
return {
82+
"keys": serialize(self.keys),
83+
"to": serialize(self.to),
84+
"expand": serialize(self.expand),
85+
"exclude": serialize(self.exclude),
86+
}
4387

4488
# noinspection PyMethodOverriding
45-
def inverse(self, data: np.ndarray, **kwargs) -> np.ndarray:
46-
data = data[0]
89+
def forward(self, data: dict[str, np.ndarray], **kwargs) -> dict[str, np.ndarray]:
90+
target_shape = data[self.to].shape
91+
92+
data = data.copy()
93+
94+
for k in self.keys:
95+
# ensure that .shape is defined
96+
data[k] = np.asarray(data[k])
97+
len_diff = len(target_shape) - len(data[k].shape)
98+
99+
if self.expand == "left":
100+
data[k] = np.expand_dims(data[k], axis=tuple(np.arange(0, len_diff)))
101+
elif self.expand == "right":
102+
data[k] = np.expand_dims(data[k], axis=tuple(-np.arange(1, len_diff + 1)))
103+
elif isinstance(self.expand, tuple):
104+
if len(self.expand) is not len_diff:
105+
raise ValueError("Length of `expand` must match the length difference of the involed arrays.")
106+
data[k] = np.expand_dims(data[k], axis=self.expand)
47107

48-
if self.expand_scalars:
49-
data = np.squeeze(data, axis=0)
108+
new_shape = target_shape
109+
if self.exclude is not None:
110+
new_shape = np.array(new_shape, dtype=int)
111+
old_shape = np.array(data[k].shape, dtype=int)
112+
exclude = list(self.exclude)
113+
new_shape[exclude] = old_shape[exclude]
114+
new_shape = tuple(new_shape)
50115

116+
data[k] = np.broadcast_to(data[k], new_shape)
117+
118+
return data
119+
120+
# noinspection PyMethodOverriding
121+
def inverse(self, data: dict[str, np.ndarray], **kwargs) -> dict[str, np.ndarray]:
122+
# TODO: add inverse
123+
# we will likely never actually need the inverse broadcasting in practice
124+
# so adding this method is not high priority
51125
return data

bayesflow/adapters/transforms/concatenate.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,23 @@
1212

1313
@serializable(package="bayesflow.adapters")
1414
class Concatenate(Transform):
15-
"""Concatenate multiple arrays into a new key."""
15+
"""Concatenate multiple arrays into a new key. Used to specify how data variables should be treated by the network.
16+
17+
Parameters:
18+
keys: Input a list of strings, where the strings are the names of data variables.
19+
into: A string telling the network how to use the variables named in keys.
20+
axis: integer specifing along which axis to concatonate the keys. The last axis is used by default.
21+
22+
Example:
23+
Suppose you have a simulator that generates variables "beta" and "sigma" from priors and then observation
24+
variables "x" and "y". We can then use concatonate in the following way
25+
26+
adapter = (
27+
bf.Adapter()
28+
.concatenate(["beta", "sigma"], into="inference_variables")
29+
.concatenate(["x", "y"], into="summary_variables")
30+
)
31+
"""
1632

1733
def __init__(self, keys: Sequence[str], *, into: str, axis: int = -1):
1834
self.keys = keys

0 commit comments

Comments
 (0)