From 6ebc5f377692afc4078be1297c9f7c046b1e03d1 Mon Sep 17 00:00:00 2001 From: LarsKue Date: Tue, 11 Mar 2025 16:38:54 +0100 Subject: [PATCH 01/10] add numpy transform --- bayesflow/adapters/transforms/__init__.py | 1 + .../adapters/transforms/numpy_transform.py | 81 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 bayesflow/adapters/transforms/numpy_transform.py diff --git a/bayesflow/adapters/transforms/__init__.py b/bayesflow/adapters/transforms/__init__.py index 1c5211d51..3f960e555 100644 --- a/bayesflow/adapters/transforms/__init__.py +++ b/bayesflow/adapters/transforms/__init__.py @@ -12,6 +12,7 @@ from .lambda_transform import LambdaTransform from .log import Log from .map_transform import MapTransform +from .numpy_transform import NumpyTransform from .one_hot import OneHot from .rename import Rename from .sqrt import Sqrt diff --git a/bayesflow/adapters/transforms/numpy_transform.py b/bayesflow/adapters/transforms/numpy_transform.py new file mode 100644 index 000000000..c110fe882 --- /dev/null +++ b/bayesflow/adapters/transforms/numpy_transform.py @@ -0,0 +1,81 @@ +import numpy as np + +from bayesflow.utils import filter_kwargs +from .elementwise_transform import ElementwiseTransform + + +class NumpyTransform(ElementwiseTransform): + """ + A class to apply element-wise transformations using plain NumPy functions. + + Attributes: + ---------- + _forward : str + The name of the NumPy function to apply in the forward transformation. + _inverse : str + The name of the NumPy function to apply in the inverse transformation. + """ + + INVERSE_METHODS = { + "arctan": "tan", + "exp": "log", + "expm1": "log1p", + "square": "sqrt", + "reciprocal": "reciprocal", + } + # ensure the map is symmetric + INVERSE_METHODS |= {v: k for k, v in INVERSE_METHODS.items()} + + def __init__(self, forward: np.ufunc | str, inverse: np.ufunc | str = None): + """ + Initializes the NumpyTransform with specified forward and inverse functions. + + Parameters: + ---------- + forward : str + The name of the NumPy function to use for the forward transformation. + inverse : str + The name of the NumPy function to use for the inverse transformation. + By default, the inverse is inferred from the forward argument for supported methods. + """ + super().__init__() + + if isinstance(forward, np.ufunc): + forward = forward.__name__ + + if inverse is None: + if forward not in self.INVERSE_METHODS: + raise ValueError(f"Cannot infer inverse for method {forward!r}") + + inverse = self.INVERSE_METHODS[forward] + elif isinstance(inverse, np.ufunc): + inverse = inverse.__name__ + + if forward not in dir(np): + raise ValueError(f"Method {forward!r} not found in numpy version {np.__version__}") + + if inverse not in dir(np): + raise ValueError(f"Method {inverse!r} not found in numpy version {np.__version__}") + + self._forward = forward + self._inverse = inverse + + @classmethod + def from_config(cls, config: dict, custom_objects=None) -> "ElementwiseTransform": + return cls( + forward=config["forward"], + inverse=config["inverse"], + ) + + def get_config(self) -> dict: + return {"forward": self._forward, "inverse": self._inverse} + + def forward(self, data: dict[str, any], **kwargs) -> dict[str, any]: + forward = getattr(np, self._forward) + kwargs = filter_kwargs(kwargs, forward) + return forward(data, **kwargs) + + def inverse(self, data: np.ndarray, **kwargs) -> np.ndarray: + inverse = getattr(np, self._inverse) + kwargs = filter_kwargs(kwargs, inverse) + return inverse(data, **kwargs) From d49aef8863823fcead58dc43d738b7c30afabd2e Mon Sep 17 00:00:00 2001 From: LarsKue Date: Tue, 11 Mar 2025 16:39:03 +0100 Subject: [PATCH 02/10] remove lambda transform --- bayesflow/adapters/transforms/__init__.py | 1 - .../adapters/transforms/lambda_transform.py | 65 ------------------- 2 files changed, 66 deletions(-) delete mode 100644 bayesflow/adapters/transforms/lambda_transform.py diff --git a/bayesflow/adapters/transforms/__init__.py b/bayesflow/adapters/transforms/__init__.py index 3f960e555..c0a1fce24 100644 --- a/bayesflow/adapters/transforms/__init__.py +++ b/bayesflow/adapters/transforms/__init__.py @@ -9,7 +9,6 @@ from .expand_dims import ExpandDims from .filter_transform import FilterTransform from .keep import Keep -from .lambda_transform import LambdaTransform from .log import Log from .map_transform import MapTransform from .numpy_transform import NumpyTransform diff --git a/bayesflow/adapters/transforms/lambda_transform.py b/bayesflow/adapters/transforms/lambda_transform.py deleted file mode 100644 index 91dc4c8a7..000000000 --- a/bayesflow/adapters/transforms/lambda_transform.py +++ /dev/null @@ -1,65 +0,0 @@ -from collections.abc import Callable -import numpy as np -from keras.saving import ( - deserialize_keras_object as deserialize, - register_keras_serializable as serializable, - serialize_keras_object as serialize, -) -from .elementwise_transform import ElementwiseTransform -from ...utils import filter_kwargs - - -@serializable(package="bayesflow.adapters") -class LambdaTransform(ElementwiseTransform): - """ - Transforms a parameter using a pair of forward and inverse functions. - - Parameters - ---------- - forward : callable, no lambda - Function to transform the data in the forward pass. - For the adapter to be serializable, this function has to be serializable - as well (see Notes). Therefore, only proper functions and no lambda - functions should be used here. - inverse : callable, no lambda - Function to transform the data in the inverse pass. - For the adapter to be serializable, this function has to be serializable - as well (see Notes). Therefore, only proper functions and no lambda - functions should be used here. - - Notes - ----- - Important: This class is only serializable if the forward and inverse functions are serializable. - This most likely means you will have to pass the scope that the forward and inverse functions are contained in - to the `custom_objects` argument of the `deserialize` function when deserializing this class. - """ - - def __init__( - self, *, forward: Callable[[np.ndarray, ...], np.ndarray], inverse: Callable[[np.ndarray, ...], np.ndarray] - ): - super().__init__() - - self._forward = forward - self._inverse = inverse - - @classmethod - def from_config(cls, config: dict, custom_objects=None) -> "LambdaTransform": - return cls( - forward=deserialize(config["forward"], custom_objects), - inverse=deserialize(config["inverse"], custom_objects), - ) - - def get_config(self) -> dict: - return { - "forward": serialize(self._forward), - "inverse": serialize(self._inverse), - } - - def forward(self, data: np.ndarray, **kwargs) -> np.ndarray: - # filter kwargs so that other transform args like batch_size, strict, ... are not passed through - kwargs = filter_kwargs(kwargs, self._forward) - return self._forward(data, **kwargs) - - def inverse(self, data: np.ndarray, **kwargs) -> np.ndarray: - kwargs = filter_kwargs(kwargs, self._inverse) - return self._inverse(data, **kwargs) From 43dd432b6da3c14a752712136816548e7f85f0b7 Mon Sep 17 00:00:00 2001 From: LarsKue Date: Tue, 11 Mar 2025 16:39:32 +0100 Subject: [PATCH 03/10] repurpose `adapter.apply()` for `NumpyTransform` instead of `LambdaTransform` --- bayesflow/adapters/adapter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bayesflow/adapters/adapter.py b/bayesflow/adapters/adapter.py index 724be81bf..1fd303959 100644 --- a/bayesflow/adapters/adapter.py +++ b/bayesflow/adapters/adapter.py @@ -1,4 +1,4 @@ -from collections.abc import Callable, MutableSequence, Sequence +from collections.abc import MutableSequence, Sequence import numpy as np from keras.saving import ( @@ -18,9 +18,9 @@ ExpandDims, FilterTransform, Keep, - LambdaTransform, Log, MapTransform, + NumpyTransform, OneHot, Rename, Sqrt, @@ -234,8 +234,8 @@ def __len__(self): def apply( self, *, - forward: Callable[[np.ndarray, ...], np.ndarray], - inverse: Callable[[np.ndarray, ...], np.ndarray], + forward: np.ufunc | str, + inverse: np.ufunc | str = None, predicate: Predicate = None, include: str | Sequence[str] = None, exclude: str | Sequence[str] = None, @@ -271,7 +271,7 @@ def apply( to the `custom_objects` argument of the `deserialize` function when deserializing this class. """ transform = FilterTransform( - transform_constructor=LambdaTransform, + transform_constructor=NumpyTransform, predicate=predicate, include=include, exclude=exclude, From b1eb4436cce48490a22b7450ad8c685b155d15c8 Mon Sep 17 00:00:00 2001 From: LarsKue Date: Tue, 11 Mar 2025 16:40:45 +0100 Subject: [PATCH 04/10] fix usages of `adapter.apply()` in example notebooks --- examples/Linear_Regression_Starter.ipynb | 62 ++++++++++++------------ examples/SIR_Posterior_Estimation.ipynb | 2 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/examples/Linear_Regression_Starter.ipynb b/examples/Linear_Regression_Starter.ipynb index 668633db0..95ff6597d 100644 --- a/examples/Linear_Regression_Starter.ipynb +++ b/examples/Linear_Regression_Starter.ipynb @@ -480,7 +480,7 @@ " .as_set([\"x\", \"y\"])\n", " .constrain(\"sigma\", lower=0)\n", " .standardize(exclude=[\"N\"])\n", - " .apply(include=\"N\", forward=lambda n: np.sqrt(n), inverse=lambda n: n**2)\n", + " .apply(include=\"N\", forward=np.sqrt)\n", " .concatenate([\"beta\", \"sigma\"], into=\"inference_variables\")\n", " .concatenate([\"x\", \"y\"], into=\"summary_variables\")\n", " .rename(\"N\", \"inference_conditions\")\n", @@ -706,65 +706,65 @@ "output_type": "stream", "text": [ "Epoch 1/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m32s\u001b[0m 226ms/step - loss: 2.6619 - loss/inference_loss: 2.6619\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m32s\u001B[0m 226ms/step - loss: 2.6619 - loss/inference_loss: 2.6619\n", "Epoch 2/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 1.5383 - loss/inference_loss: 1.5383\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 1.5383 - loss/inference_loss: 1.5383\n", "Epoch 3/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 1.2547 - loss/inference_loss: 1.2547\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 1.2547 - loss/inference_loss: 1.2547\n", "Epoch 4/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.9276 - loss/inference_loss: 0.9276\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.9276 - loss/inference_loss: 0.9276\n", "Epoch 5/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.7913 - loss/inference_loss: 0.7913\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.7913 - loss/inference_loss: 0.7913\n", "Epoch 6/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.6641 - loss/inference_loss: 0.6641\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.6641 - loss/inference_loss: 0.6641\n", "Epoch 7/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.5110 - loss/inference_loss: 0.5110\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.5110 - loss/inference_loss: 0.5110\n", "Epoch 8/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.4761 - loss/inference_loss: 0.4761\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.4761 - loss/inference_loss: 0.4761\n", "Epoch 9/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.3466 - loss/inference_loss: 0.3466\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.3466 - loss/inference_loss: 0.3466\n", "Epoch 10/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.2320 - loss/inference_loss: 0.2320\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.2320 - loss/inference_loss: 0.2320\n", "Epoch 11/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.2532 - loss/inference_loss: 0.2532\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.2532 - loss/inference_loss: 0.2532\n", "Epoch 12/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.1257 - loss/inference_loss: 0.1257\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.1257 - loss/inference_loss: 0.1257\n", "Epoch 13/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.1499 - loss/inference_loss: 0.1499\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.1499 - loss/inference_loss: 0.1499\n", "Epoch 14/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.0730 - loss/inference_loss: 0.0730\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.0730 - loss/inference_loss: 0.0730\n", "Epoch 15/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.0074 - loss/inference_loss: 0.0074\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.0074 - loss/inference_loss: 0.0074\n", "Epoch 16/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.0532 - loss/inference_loss: 0.0532\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.0532 - loss/inference_loss: 0.0532\n", "Epoch 17/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: 0.0035 - loss/inference_loss: 0.0035\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: 0.0035 - loss/inference_loss: 0.0035\n", "Epoch 18/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.0339 - loss/inference_loss: -0.0339\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.0339 - loss/inference_loss: -0.0339\n", "Epoch 19/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 13ms/step - loss: -0.1002 - loss/inference_loss: -0.1002\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 13ms/step - loss: -0.1002 - loss/inference_loss: -0.1002\n", "Epoch 20/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.1655 - loss/inference_loss: -0.1655\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.1655 - loss/inference_loss: -0.1655\n", "Epoch 21/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.0874 - loss/inference_loss: -0.0874\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.0874 - loss/inference_loss: -0.0874\n", "Epoch 22/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.1598 - loss/inference_loss: -0.1598\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.1598 - loss/inference_loss: -0.1598\n", "Epoch 23/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.0770 - loss/inference_loss: -0.0770\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.0770 - loss/inference_loss: -0.0770\n", "Epoch 24/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.1823 - loss/inference_loss: -0.1823\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.1823 - loss/inference_loss: -0.1823\n", "Epoch 25/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.1833 - loss/inference_loss: -0.1833\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.1833 - loss/inference_loss: -0.1833\n", "Epoch 26/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.2606 - loss/inference_loss: -0.2606\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.2606 - loss/inference_loss: -0.2606\n", "Epoch 27/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 13ms/step - loss: -0.1786 - loss/inference_loss: -0.1786\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 13ms/step - loss: -0.1786 - loss/inference_loss: -0.1786\n", "Epoch 28/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.2828 - loss/inference_loss: -0.2828\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.2828 - loss/inference_loss: -0.2828\n", "Epoch 29/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 13ms/step - loss: -0.1857 - loss/inference_loss: -0.1857\n", + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 13ms/step - loss: -0.1857 - loss/inference_loss: -0.1857\n", "Epoch 30/30\n", - "\u001b[1m128/128\u001b[0m \u001b[32m━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[37m\u001b[0m \u001b[1m2s\u001b[0m 12ms/step - loss: -0.1761 - loss/inference_loss: -0.1761\n" + "\u001B[1m128/128\u001B[0m \u001B[32m━━━━━━━━━━━━━━━━━━━━\u001B[0m\u001B[37m\u001B[0m \u001B[1m2s\u001B[0m 12ms/step - loss: -0.1761 - loss/inference_loss: -0.1761\n" ] } ], diff --git a/examples/SIR_Posterior_Estimation.ipynb b/examples/SIR_Posterior_Estimation.ipynb index 4a4b1786d..15e478a47 100644 --- a/examples/SIR_Posterior_Estimation.ipynb +++ b/examples/SIR_Posterior_Estimation.ipynb @@ -375,7 +375,7 @@ " # since all our variables are non-negative (zero or larger)\n", " # this .apply call ensures that the variables are transformed\n", " # to the unconstrained real space and can be back-transformed under the hood\n", - " .apply(forward=lambda x: np.log1p(x), inverse=lambda x: np.expm1(x))\n", + " .apply(forward=np.log1p)\n", ")" ] }, From f5314352d43d72406a14a2c48275f450530252c1 Mon Sep 17 00:00:00 2001 From: LarsKue Date: Thu, 13 Mar 2025 16:07:31 +0100 Subject: [PATCH 05/10] fix tests --- tests/test_adapters/conftest.py | 16 ++-------------- tests/test_adapters/test_adapters.py | 14 +++++++------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/tests/test_adapters/conftest.py b/tests/test_adapters/conftest.py index 03f214578..929f81e2a 100644 --- a/tests/test_adapters/conftest.py +++ b/tests/test_adapters/conftest.py @@ -2,19 +2,6 @@ import pytest -def forward_transform(x): - return x + 1 - - -def inverse_transform(x): - return x - 1 - - -@pytest.fixture() -def custom_objects(): - return dict(forward_transform=forward_transform, inverse_transform=inverse_transform) - - @pytest.fixture() def adapter(): from bayesflow.adapters import Adapter @@ -29,9 +16,10 @@ def adapter(): .concatenate(["x1", "x2"], into="x") .concatenate(["y1", "y2"], into="y") .expand_dims(["z1"], axis=2) - .apply(forward=forward_transform, inverse=inverse_transform) .log("p1") .constrain("p2", lower=0) + .apply(include="p2", forward=np.exp, inverse=np.log) + .apply(include="p2", forward="logp1") .standardize(exclude=["t1", "t2", "o1"]) .drop("d1") .one_hot("o1", 10) diff --git a/tests/test_adapters/test_adapters.py b/tests/test_adapters/test_adapters.py index efd58bd6e..840a71ba2 100644 --- a/tests/test_adapters/test_adapters.py +++ b/tests/test_adapters/test_adapters.py @@ -17,10 +17,10 @@ def test_cycle_consistency(adapter, random_data): assert np.allclose(value, deprocessed[key]) -def test_serialize_deserialize(adapter, custom_objects, random_data): +def test_serialize_deserialize(adapter, random_data): processed = adapter(random_data) serialized = serialize(adapter) - deserialized = deserialize(serialized, custom_objects) + deserialized = deserialize(serialized) reserialized = serialize(deserialized) assert reserialized.keys() == serialized.keys() @@ -51,7 +51,7 @@ def test_constrain(): "x_both_disc2": np.vstack((np.zeros(shape=(16, 1)), np.ones(shape=(16, 1)))), } - adapter = ( + ad = ( Adapter() .constrain("x_lower_cont", lower=0) .constrain("x_upper_cont", upper=0) @@ -66,7 +66,7 @@ def test_constrain(): with warnings.catch_warnings(): warnings.simplefilter("ignore", RuntimeWarning) - result = adapter(data) + result = ad(data) # continuous variables should not have boundary issues assert result["x_lower_cont"].min() < 0.0 @@ -93,9 +93,9 @@ def test_simple_transforms(random_data): # check if simple transforms are applied correctly from bayesflow.adapters import Adapter - adapter = Adapter().log(["p2", "t2"]).log("t1", p1=True).sqrt("p1") + ad = Adapter().log(["p2", "t2"]).log("t1", p1=True).sqrt("p1") - result = adapter(random_data) + result = ad(random_data) assert np.array_equal(result["p2"], np.log(random_data["p2"])) assert np.array_equal(result["t2"], np.log(random_data["t2"])) @@ -103,7 +103,7 @@ def test_simple_transforms(random_data): assert np.array_equal(result["p1"], np.sqrt(random_data["p1"])) # inverse results should match the original input - inverse = adapter.inverse(result) + inverse = ad(result, inverse=True) assert np.array_equal(inverse["p2"], random_data["p2"]) assert np.array_equal(inverse["t2"], random_data["t2"]) From 53d48d4cc68b05128edeb69ccf985b720b3b5f11 Mon Sep 17 00:00:00 2001 From: larskue Date: Mon, 17 Mar 2025 11:48:43 +0100 Subject: [PATCH 06/10] only allow strings as arguments (subject to be fixed by #323) --- .../adapters/transforms/numpy_transform.py | 47 ++++++++++--------- tests/test_adapters/conftest.py | 4 +- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/bayesflow/adapters/transforms/numpy_transform.py b/bayesflow/adapters/transforms/numpy_transform.py index c110fe882..0e4eaa7f2 100644 --- a/bayesflow/adapters/transforms/numpy_transform.py +++ b/bayesflow/adapters/transforms/numpy_transform.py @@ -1,9 +1,11 @@ import numpy as np +from keras.saving import register_keras_serializable as serializable from bayesflow.utils import filter_kwargs from .elementwise_transform import ElementwiseTransform +@serializable(package="bayesflow.adapters") class NumpyTransform(ElementwiseTransform): """ A class to apply element-wise transformations using plain NumPy functions. @@ -17,45 +19,46 @@ class NumpyTransform(ElementwiseTransform): """ INVERSE_METHODS = { - "arctan": "tan", - "exp": "log", - "expm1": "log1p", - "square": "sqrt", - "reciprocal": "reciprocal", + np.arctan: np.tan, + np.exp: np.log, + np.expm1: np.log1p, + np.square: np.sqrt, + np.reciprocal: np.reciprocal, } # ensure the map is symmetric INVERSE_METHODS |= {v: k for k, v in INVERSE_METHODS.items()} - def __init__(self, forward: np.ufunc | str, inverse: np.ufunc | str = None): + def __init__(self, forward: str, inverse: str = None): """ Initializes the NumpyTransform with specified forward and inverse functions. Parameters: ---------- - forward : str + forward: str The name of the NumPy function to use for the forward transformation. - inverse : str + inverse: str, optional The name of the NumPy function to use for the inverse transformation. By default, the inverse is inferred from the forward argument for supported methods. """ super().__init__() - if isinstance(forward, np.ufunc): - forward = forward.__name__ + if isinstance(forward, str): + forward = getattr(np, forward) + + if not isinstance(forward, np.ufunc): + raise ValueError("Forward transformation must be a NumPy Universal Function (ufunc).") if inverse is None: if forward not in self.INVERSE_METHODS: raise ValueError(f"Cannot infer inverse for method {forward!r}") inverse = self.INVERSE_METHODS[forward] - elif isinstance(inverse, np.ufunc): - inverse = inverse.__name__ - if forward not in dir(np): - raise ValueError(f"Method {forward!r} not found in numpy version {np.__version__}") + if isinstance(inverse, str): + inverse = getattr(np, inverse) - if inverse not in dir(np): - raise ValueError(f"Method {inverse!r} not found in numpy version {np.__version__}") + if not isinstance(inverse, np.ufunc): + raise ValueError("Inverse transformation must be a NumPy Universal Function (ufunc).") self._forward = forward self._inverse = inverse @@ -68,14 +71,12 @@ def from_config(cls, config: dict, custom_objects=None) -> "ElementwiseTransform ) def get_config(self) -> dict: - return {"forward": self._forward, "inverse": self._inverse} + return {"forward": self._forward.__name__, "inverse": self._inverse.__name__} def forward(self, data: dict[str, any], **kwargs) -> dict[str, any]: - forward = getattr(np, self._forward) - kwargs = filter_kwargs(kwargs, forward) - return forward(data, **kwargs) + kwargs = filter_kwargs(kwargs, self._forward) + return self._forward(data, **kwargs) def inverse(self, data: np.ndarray, **kwargs) -> np.ndarray: - inverse = getattr(np, self._inverse) - kwargs = filter_kwargs(kwargs, inverse) - return inverse(data, **kwargs) + kwargs = filter_kwargs(kwargs, self._inverse) + return self._inverse(data, **kwargs) diff --git a/tests/test_adapters/conftest.py b/tests/test_adapters/conftest.py index 929f81e2a..b020523d9 100644 --- a/tests/test_adapters/conftest.py +++ b/tests/test_adapters/conftest.py @@ -18,8 +18,8 @@ def adapter(): .expand_dims(["z1"], axis=2) .log("p1") .constrain("p2", lower=0) - .apply(include="p2", forward=np.exp, inverse=np.log) - .apply(include="p2", forward="logp1") + .apply(include="p2", forward="exp", inverse="log") + .apply(include="p2", forward="log1p") .standardize(exclude=["t1", "t2", "o1"]) .drop("d1") .one_hot("o1", 10) From 94b8c99f6f282685f31898a60f35aa23660bd928 Mon Sep 17 00:00:00 2001 From: larskue Date: Mon, 17 Mar 2025 11:48:58 +0100 Subject: [PATCH 07/10] unify serialization pattern --- bayesflow/adapters/transforms/as_set.py | 2 ++ bayesflow/adapters/transforms/as_time_series.py | 2 ++ bayesflow/adapters/transforms/expand_dims.py | 3 ++- bayesflow/adapters/transforms/log.py | 3 ++- bayesflow/adapters/transforms/sqrt.py | 2 ++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/bayesflow/adapters/transforms/as_set.py b/bayesflow/adapters/transforms/as_set.py index 2e0fe86e1..6e8e5567e 100644 --- a/bayesflow/adapters/transforms/as_set.py +++ b/bayesflow/adapters/transforms/as_set.py @@ -1,8 +1,10 @@ import numpy as np +from keras.saving import register_keras_serializable as serializable from .elementwise_transform import ElementwiseTransform +@serializable(package="bayesflow.adapters") class AsSet(ElementwiseTransform): """The `.as_set(["x", "y"])` transform indicates that both `x` and `y` are treated as sets. diff --git a/bayesflow/adapters/transforms/as_time_series.py b/bayesflow/adapters/transforms/as_time_series.py index 201181547..9453af817 100644 --- a/bayesflow/adapters/transforms/as_time_series.py +++ b/bayesflow/adapters/transforms/as_time_series.py @@ -1,8 +1,10 @@ import numpy as np +from keras.saving import register_keras_serializable as serializable from .elementwise_transform import ElementwiseTransform +@serializable(package="bayesflow.adapters") class AsTimeSeries(ElementwiseTransform): """The `.as_time_series` transform can be used to indicate that variables shall be treated as time series. diff --git a/bayesflow/adapters/transforms/expand_dims.py b/bayesflow/adapters/transforms/expand_dims.py index 6a9519d8e..97c106f51 100644 --- a/bayesflow/adapters/transforms/expand_dims.py +++ b/bayesflow/adapters/transforms/expand_dims.py @@ -1,13 +1,14 @@ import numpy as np - from keras.saving import ( deserialize_keras_object as deserialize, serialize_keras_object as serialize, ) +from keras.saving import register_keras_serializable as serializable from .elementwise_transform import ElementwiseTransform +@serializable(package="bayesflow.adapters") class ExpandDims(ElementwiseTransform): """ Expand the shape of an array. diff --git a/bayesflow/adapters/transforms/log.py b/bayesflow/adapters/transforms/log.py index cefe468b2..9c5325da3 100644 --- a/bayesflow/adapters/transforms/log.py +++ b/bayesflow/adapters/transforms/log.py @@ -1,13 +1,14 @@ import numpy as np - from keras.saving import ( deserialize_keras_object as deserialize, serialize_keras_object as serialize, ) +from keras.saving import register_keras_serializable as serializable from .elementwise_transform import ElementwiseTransform +@serializable(package="bayesflow.adapters") class Log(ElementwiseTransform): """Log transforms a variable. diff --git a/bayesflow/adapters/transforms/sqrt.py b/bayesflow/adapters/transforms/sqrt.py index 88bb81a08..b9f693e75 100644 --- a/bayesflow/adapters/transforms/sqrt.py +++ b/bayesflow/adapters/transforms/sqrt.py @@ -1,8 +1,10 @@ import numpy as np +from keras.saving import register_keras_serializable as serializable from .elementwise_transform import ElementwiseTransform +@serializable(package="bayesflow.adapters") class Sqrt(ElementwiseTransform): """Square-root transform a variable. From 96a21c95f8f03a9d1f4298dfc44d88a3493f283a Mon Sep 17 00:00:00 2001 From: larskue Date: Mon, 17 Mar 2025 11:51:37 +0100 Subject: [PATCH 08/10] remove old lambda transform error message --- bayesflow/adapters/transforms/filter_transform.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/bayesflow/adapters/transforms/filter_transform.py b/bayesflow/adapters/transforms/filter_transform.py index 1a3d40b7a..65f5750c8 100644 --- a/bayesflow/adapters/transforms/filter_transform.py +++ b/bayesflow/adapters/transforms/filter_transform.py @@ -86,18 +86,6 @@ def from_config(cls, config: dict, custom_objects=None) -> "Transform": try: kwargs = deserialize(config["kwargs"]) except TypeError as e: - if transform_constructor.__name__ == "LambdaTransform": - raise TypeError( - "LambdaTransform (created by Adapter.apply) could not be deserialized.\n" - "This is probably because the custom transform functions `forward` and " - "`backward` from `Adapter.apply` were not passed as `custom_objects`.\n" - "For example, if your adapter uses\n" - "`Adapter.apply(forward=forward_transform, inverse=inverse_transform)`,\n" - "you have to pass\n" - '`custom_objects={"forward_transform": forward_transform, ' - '"inverse_transform": inverse_transform}`\n' - "to the function you use to load the serialized object." - ) from e raise TypeError( "The transform could not be deserialized properly. " "The most likely reason is that some classes or functions " From d8bab7bffef75536f87c0a0ab58e5c2aab9c457e Mon Sep 17 00:00:00 2001 From: larskue Date: Mon, 17 Mar 2025 11:58:52 +0100 Subject: [PATCH 09/10] add fail fast to CI tests --- .github/workflows/tests.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 6e8175fb8..bfd5583cd 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -95,7 +95,7 @@ jobs: - name: Run Tests run: | - pytest + pytest -x - name: Create Coverage Report run: | From 54e939875a06c1e523434e50c7a962e00c3e0022 Mon Sep 17 00:00:00 2001 From: larskue Date: Mon, 17 Mar 2025 12:48:52 +0100 Subject: [PATCH 10/10] cannot suppport filtering kwargs for numpy ufunc in python 3.10 --- bayesflow/adapters/transforms/expand_dims.py | 2 +- bayesflow/adapters/transforms/log.py | 2 +- bayesflow/adapters/transforms/numpy_transform.py | 7 ++----- bayesflow/adapters/transforms/to_array.py | 4 +--- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/bayesflow/adapters/transforms/expand_dims.py b/bayesflow/adapters/transforms/expand_dims.py index 97c106f51..eb4d712f4 100644 --- a/bayesflow/adapters/transforms/expand_dims.py +++ b/bayesflow/adapters/transforms/expand_dims.py @@ -1,9 +1,9 @@ import numpy as np from keras.saving import ( deserialize_keras_object as deserialize, + register_keras_serializable as serializable, serialize_keras_object as serialize, ) -from keras.saving import register_keras_serializable as serializable from .elementwise_transform import ElementwiseTransform diff --git a/bayesflow/adapters/transforms/log.py b/bayesflow/adapters/transforms/log.py index 9c5325da3..e264fccfa 100644 --- a/bayesflow/adapters/transforms/log.py +++ b/bayesflow/adapters/transforms/log.py @@ -1,9 +1,9 @@ import numpy as np from keras.saving import ( deserialize_keras_object as deserialize, + register_keras_serializable as serializable, serialize_keras_object as serialize, ) -from keras.saving import register_keras_serializable as serializable from .elementwise_transform import ElementwiseTransform diff --git a/bayesflow/adapters/transforms/numpy_transform.py b/bayesflow/adapters/transforms/numpy_transform.py index 0e4eaa7f2..817d98b17 100644 --- a/bayesflow/adapters/transforms/numpy_transform.py +++ b/bayesflow/adapters/transforms/numpy_transform.py @@ -1,7 +1,6 @@ import numpy as np from keras.saving import register_keras_serializable as serializable -from bayesflow.utils import filter_kwargs from .elementwise_transform import ElementwiseTransform @@ -74,9 +73,7 @@ def get_config(self) -> dict: return {"forward": self._forward.__name__, "inverse": self._inverse.__name__} def forward(self, data: dict[str, any], **kwargs) -> dict[str, any]: - kwargs = filter_kwargs(kwargs, self._forward) - return self._forward(data, **kwargs) + return self._forward(data) def inverse(self, data: np.ndarray, **kwargs) -> np.ndarray: - kwargs = filter_kwargs(kwargs, self._inverse) - return self._inverse(data, **kwargs) + return self._inverse(data) diff --git a/bayesflow/adapters/transforms/to_array.py b/bayesflow/adapters/transforms/to_array.py index 393d31b6e..aefb51040 100644 --- a/bayesflow/adapters/transforms/to_array.py +++ b/bayesflow/adapters/transforms/to_array.py @@ -1,9 +1,7 @@ from numbers import Number import numpy as np -from keras.saving import ( - register_keras_serializable as serializable, -) +from keras.saving import register_keras_serializable as serializable from bayesflow.utils.io import deserialize_type, serialize_type from .elementwise_transform import ElementwiseTransform