Source code for bayesflow.networks.inference.diffusion.diffusion_model

from collections.abc import Sequence, Mapping
from typing import Any, Literal, Callable

import keras
from keras import ops

from bayesflow._backend import grad
from bayesflow.distributions import DiagonalNormal
from bayesflow.types import Tensor, Shape
from bayesflow.utils import (
    expand_right_as,
    find_network,
    integrate,
    integrate_stochastic,
    jacobian_trace,
    logging,
    linsolve_batched,
    repeat_and_flatten,
    resolve_seed,
    vjp,
    weighted_mean,
    DETERMINISTIC_METHODS,
    STOCHASTIC_METHODS,
)
from bayesflow.utils.jacobian.jacobian_trace import hutchinson_projectors
from bayesflow.utils.serialization import serialize, serializable

from .schedules.noise_schedule import NoiseSchedule
from .dispatch import find_noise_schedule

from ...inference import InferenceNetwork
from bayesflow.masking import InferenceMasks, MaskHandler
from ...defaults import TIME_MLP_DEFAULTS, DIFFUSION_TRANSFORMER_DEFAULTS, DIFFUSION_INTEGRATE_DEFAULTS


[docs] @serializable("bayesflow.networks") class DiffusionModel(InferenceNetwork): """Score-based diffusion model for simulation-based inference (SBI). Implements a score-based diffusion model with configurable subnet architecture, noise schedule, and prediction/loss types for amortized SBI as described in [1]. The diffusion model allows for post-hoc guidance (see [1]) and composition [2], implementing, stabilizing, and scaling initial ideas from [3] and [4]. To learn arbitrary conditional distributions, the model can be trained with a transformer backbone and masking based on ideas from [5]. Note that score-based diffusion is the most sluggish of all available samplers, so expect slower inference times than flow matching and much slower than normalizing flows. Parameters ---------- subnet : str, type, or keras.Layer A neural network type for the diffusion model, will be instantiated using *subnet_kwargs*. If a string is provided, it should be a registered name (e.g., ``"time_mlp"``, ``"diffusion_transformer"``). If a type or ``keras.Layer`` is provided, it will be directly instantiated with the given *subnet_kwargs*. Any subnet must accept a tuple of tensors ``(target, time, conditions)``. noise_schedule : {'edm', 'cosine'} or NoiseSchedule or type Noise schedule controlling the diffusion dynamics. Can be a string identifier, a schedule class, or a pre-initialised schedule instance. Default is ``"cosine"``. prediction_type : {'velocity', 'noise', 'F', 'x', 'score', 'potential'} Output format of the model's prediction. Default is ``"velocity"``. loss_type : {'velocity', 'noise', 'F'} Loss function used to train the model. Default is ``"noise"``. subnet_kwargs : dict[str, Any], optional Additional keyword arguments passed to the subnet constructor. schedule_kwargs : dict[str, Any], optional Additional keyword arguments passed to the noise schedule constructor. integrate_kwargs : dict[str, Any], optional Configuration dictionary for the ODE/SDE integrator used at inference time. mask_handler : ~bayesflow.masking.MaskHandler, optional Handler used to generate and route masks. If ``None``, a default mask handler is created. **kwargs Additional keyword arguments passed to the base ``InferenceNetwork``. References ---------- [1] Arruda, J., Bracher, N., Köthe, U., Hasenauer, J., & Radev, S. T. (2026). Diffusion Models in Simulation-Based Inference: A Tutorial Review. https://openreview.net/forum?id=86Q261z005 [2] Arruda et al., (2025). Compositional amortized inference for large-scale hierarchical Bayesian models. https://arxiv.org/abs/2505.14429 [3] Geffner et al., (2023). Compositional score modeling for simulation-based inference. https://arxiv.org/abs/2209.14249 [4] Linhart et al., (2024). Diffusion posterior sampling for simulation-based inference in tall data settings. https://arxiv.org/abs/2404.07593 [5] Gloeckler et al. (2024). All-in-one simulation-based inference. https://dl.acm.org/doi/proceedings/10.5555/3692070 """ def __init__( self, *, subnet: str | type | keras.Layer = "time_mlp", noise_schedule: Literal["edm", "cosine"] | NoiseSchedule | type = "cosine", prediction_type: Literal["velocity", "noise", "F", "x", "score", "potential"] = "velocity", loss_type: Literal["velocity", "noise", "F"] = "noise", subnet_kwargs: dict[str, Any] = None, schedule_kwargs: dict[str, Any] = None, integrate_kwargs: dict[str, Any] = None, mask_handler: MaskHandler | None = None, **kwargs, ): noise_schedule = find_noise_schedule(noise_schedule, **(schedule_kwargs or {})) base_distribution = "normal" if noise_schedule._variance_type == "exploding": _, sigma_max = noise_schedule.get_alpha_sigma(noise_schedule.log_snr(1.0, training=False)) base_distribution = DiagonalNormal(std=float(ops.convert_to_numpy(sigma_max))) super().__init__( base_distribution=base_distribution, mask_handler=mask_handler, **kwargs, ) if prediction_type not in ["noise", "velocity", "F", "x", "score", "potential"]: raise ValueError(f"Unknown prediction type: {prediction_type}") if loss_type not in ["noise", "velocity", "F"]: raise ValueError(f"Unknown loss type: {loss_type}") self._prediction_type = prediction_type self._loss_type = loss_type self.noise_schedule = noise_schedule self.noise_schedule.validate(loss_type=loss_type) self.integrate_kwargs = DIFFUSION_INTEGRATE_DEFAULTS | (integrate_kwargs or {}) subnet_defaults = {"time_mlp": TIME_MLP_DEFAULTS, "diffusion_transformer": DIFFUSION_TRANSFORMER_DEFAULTS} self.subnet = find_network(subnet, **(subnet_defaults.get(subnet, {}) | (subnet_kwargs or {}))) self.output_projector = None self.compositional_bridge_d0 = 1.0 self.compositional_bridge_d1 = 1.0
[docs] def compute_metrics( self, x: Tensor | Sequence[Tensor], conditions: Tensor = None, sample_weight: Tensor = None, stage: str = "training", **kwargs, ) -> dict[str, Tensor]: training = stage == "training" noise_schedule_training_stage = stage == "training" or stage == "validation" # Sample training diffusion time as a low discrepancy sequence to decrease variance u0 = keras.random.uniform(shape=(1,), dtype=ops.dtype(x), seed=self.seed_generator) i = ops.arange(0, ops.shape(x)[0], dtype=ops.dtype(x)) t = (u0 + i / ops.cast(ops.shape(x)[0], dtype=ops.dtype(x))) % 1 # Calculate the noise level log_snr_t = self.noise_schedule.log_snr(t, training=noise_schedule_training_stage) log_snr_t = expand_right_as(log_snr_t, x) alpha_t, sigma_t = self.noise_schedule.get_alpha_sigma(log_snr_t=log_snr_t) weights_for_snr = self.noise_schedule.weights_for_snr(log_snr_t=log_snr_t, loss_type=self._loss_type) # Generate noise vector eps_t = keras.random.normal(ops.shape(x), dtype=ops.dtype(x), seed=self.seed_generator) # Diffuse x to get noisy input to the network diffused_x = alpha_t * x + sigma_t * eps_t masks = self.mask_handler.resolve( kwargs, targets=x, conditions=conditions, stage=stage, subnet=self.subnet, ) # Fixed targets remain clean while inferred targets are diffused. diffused_x = self.mask_handler.maybe_apply(diffused_x, masks.fixed_target_mask, replacement=x) x_pred = self._predict_x(diffused_x, log_snr_t, alpha_t, sigma_t, conditions, training, masks.subnet_kwargs) # Finally, compute the loss according to the configured loss type match self._loss_type: case "noise": noise_pred = (diffused_x - alpha_t * x_pred) / sigma_t error = (noise_pred - eps_t) ** 2 case "velocity": noise_pred = (diffused_x - alpha_t * x_pred) / sigma_t velocity_pred = alpha_t * noise_pred - sigma_t * x_pred v_t = alpha_t * eps_t - sigma_t * x error = (velocity_pred - v_t) ** 2 case "F": sigma_data = getattr(self.noise_schedule, "sigma_data", 1.0) x1 = ops.sqrt(ops.exp(-log_snr_t) + sigma_data**2) / (ops.exp(-log_snr_t / 2) * sigma_data) error = (x1 * (x_pred - x)) ** 2 case _: raise ValueError(f"Unknown loss type: {self._loss_type}") error = self.mask_handler.maybe_apply(error, masks.fixed_target_mask) weights_for_snr = ops.reshape(weights_for_snr, (ops.shape(x)[0],)) loss = weights_for_snr * ops.mean(error, axis=tuple(range(1, ops.ndim(error)))) loss = weighted_mean(loss, sample_weight) return {"loss": loss}
[docs] def build(self, xz_shape: Shape, conditions_shape: Shape = None): if self.built: return self.base_distribution.build(xz_shape) units = 1 if self._prediction_type == "potential" else xz_shape[-1] self.output_projector = self._build_subnet_and_projector(xz_shape, conditions_shape, units=units)
[docs] def get_config(self): base_config = super().get_config() config = { "subnet": self.subnet, "noise_schedule": self.noise_schedule, "prediction_type": self._prediction_type, "loss_type": self._loss_type, "integrate_kwargs": self.integrate_kwargs, } return base_config | serialize(config)
[docs] def guidance_function( self, x_pred: Tensor, time: Tensor, score: Tensor, constraints: Callable | Sequence[Callable] = None, guidance_strength: float = 1.0, scaling_function: Callable = None, reduce: Literal["sum", "mean"] = "sum", unstandardize: Callable = None, ) -> Tensor: """ Takes the current denoised estimate and the diffusion time. By default, it computes a guidance constraint term if guidance constraints are provided, and returns the score otherwise. Function can be overwritten by the user to implement custom guidance. Custom guidance: def guidance_function(x_pred, time, score, unstandardize=None, **kwargs): # x_pred is in the standardized space; call unstandardize(x_pred) to map it # back to the original parameter space. workflow.estimator.inference_network.guidance_function = guidance_function Parameters ---------- x_pred : Tensor The denoised target at time t. time : Tensor The time corresponding to x. score: Tensor The score at the current time point, which should be guided. constraints : Callable or Sequence[Callable], optional A single constraint function or a list/tuple of constraint functions. Each function should take x as input and return a tensor of constraint values. guidance_strength : float A positive scaling factor for the guidance term. Default is 1.0. scaling_function : Callable, optional A function that takes time t as input and returns a scaling factor s(t). If None, a default scaling based on the noise schedule is used. Default is None. reduce : {'sum', 'mean'} Method to reduce the log-probabilities from multiple constraints. Default is 'sum'. unstandardize : Callable, optional A function mapping the standardized denoised estimate back to the original (unstandardized) inference-variable space. When set, constraints are evaluated on the unstandardized estimate, so they can be expressed directly in the original parameter space. Injected automatically by the estimator during sampling; the gradient flows back through the transformation. Default is None. Returns ------- Tensor The computed guidance term of the same shape as zt. """ if constraints is None: return score if not isinstance(constraints, Sequence): constraints = [constraints] if scaling_function is None: def scaling_function(t: Tensor) -> Tensor: log_snr = self.noise_schedule.log_snr(t, training=False) alpha_t, sigma_t = self.noise_schedule.get_alpha_sigma(log_snr) return ops.square(alpha_t) / ops.square(sigma_t) if unstandardize is None: def unstandardize(x): return x def objective_fn(x: Tensor) -> Tensor: st = scaling_function(time) x = unstandardize(x) logp = keras.ops.zeros((), dtype=x.dtype) for c in constraints: ck = c(x) logp = logp - keras.ops.softplus(st * ck) return keras.ops.sum(logp) if reduce == "sum" else keras.ops.mean(logp) grad_val = grad(objective_fn)(x_pred) return score + guidance_strength * grad_val
[docs] def convert_prediction_to_x( self, pred: Tensor, z: Tensor, alpha_t: Tensor, sigma_t: Tensor, log_snr_t: Tensor, prediction_type: str ) -> Tensor: """ Converts the neural network prediction into the denoised data `x`, depending on the prediction type configured for the model. Parameters ---------- pred : Tensor The output prediction from the neural network, typically representing noise, velocity, or a transformation of the clean signal. z : Tensor The noisy latent variable `z` to be denoised. alpha_t : Tensor The noise schedule's scaling factor for the clean signal at time `t`. sigma_t : Tensor The standard deviation of the noise at time `t`. log_snr_t : Tensor The log signal-to-noise ratio at time `t`. prediction_type : str The type of prediction made by the model, which determines how to convert it to `x`. Must be one of {'velocity', 'noise', 'F', 'x', 'score'}. Returns ------- Tensor The reconstructed clean signal `x` from the model prediction. """ match prediction_type: case "velocity": return (alpha_t * z - sigma_t * pred) / (alpha_t**2 + sigma_t**2) case "noise": return (z - sigma_t * pred) / alpha_t case "F": sigma_data = getattr(self.noise_schedule, "sigma_data", 1.0) x1 = (sigma_data**2 * alpha_t) / (ops.exp(-log_snr_t) + sigma_data**2) x2 = ops.exp(-log_snr_t / 2) * sigma_data / ops.sqrt(ops.exp(-log_snr_t) + sigma_data**2) return x1 * z + x2 * pred case "x": return pred case "score": return (z + sigma_t**2 * pred) / alpha_t case "potential": # Score is computed as ∇_z phi via _compute_grad_of_potential. raise RuntimeError("convert_prediction_to_x should not be called for prediction_type='potential'. ") case _: raise ValueError(f"Unknown prediction type {prediction_type}.")
[docs] def score( self, xz: Tensor, time: float | Tensor = None, log_snr_t: Tensor = None, conditions: Tensor = None, training: bool = False, guidance_kwargs: Mapping[str, Any] = None, **kwargs, ) -> Tensor: """ Computes the score of the target or latent variable `xz`. Parameters ---------- xz : Tensor The current state of the latent variable `z`, typically of shape (..., D), where D is the dimensionality of the latent space. time : float or Tensor Scalar or tensor representing the time (or noise level) at which the velocity should be computed. Will be broadcasted to xz. If None, log_snr_t must be provided. log_snr_t : Tensor The log signal-to-noise ratio at time `t`. If None, time must be provided. conditions : Tensor, optional Conditional inputs to the network, such as conditioning variables or encoder outputs. Shape must be broadcastable with `xz`. Default is None. training : bool, optional Whether the model is in training mode. Affects behavior of dropout, batch norm, or other stochastic layers. Default is False. guidance_kwargs : Mapping[str, Any], optional A dictionary of parameters for computing a guidance constraint term, which is added to the score for guided sampling. The specific keys and values depend on the implementation of `guidance_function`. **kwargs Subnet kwargs (e.g., attention_mask, mask) for the subnet layer. Returns ------- Tensor The velocity tensor of the same shape as `xz`, representing the right-hand side of the probability-flow SDE or ODE at the given `time`. """ masks = self.mask_handler.resolve( kwargs, targets=xz, conditions=conditions, subnet=self.subnet, ) return self._score( xz, time=time, log_snr_t=log_snr_t, conditions=conditions, training=training, guidance_kwargs=guidance_kwargs, masks=masks, )
def _predict_x( self, xz: Tensor, log_snr_t: Tensor, alpha_t: Tensor, sigma_t: Tensor, conditions: Tensor, training: bool, subnet_kwargs: dict, ) -> Tensor: """Predict the clean signal x from the noisy xz with the configured prediction type.""" norm_log_snr = self._transform_log_snr(log_snr_t) if self._prediction_type == "potential": pred = self._compute_grad_of_potential( xz=xz, norm_log_snr=norm_log_snr, conditions=conditions, training=training, **subnet_kwargs ) prediction_type = "velocity" else: subnet_out = self.subnet((xz, norm_log_snr, conditions), training=training, **subnet_kwargs) pred = self.output_projector(subnet_out) prediction_type = self._prediction_type return self.convert_prediction_to_x( pred=pred, z=xz, alpha_t=alpha_t, sigma_t=sigma_t, log_snr_t=log_snr_t, prediction_type=prediction_type ) def _score( self, xz: Tensor, *, masks: InferenceMasks, time: float | Tensor = None, log_snr_t: Tensor = None, conditions: Tensor = None, training: bool = False, guidance_kwargs: Mapping[str, Any] = None, ) -> Tensor: if log_snr_t is None: log_snr_t = self.noise_schedule.log_snr(t=time, training=training) log_snr_t = expand_right_as(log_snr_t, xz) log_snr_t = ops.broadcast_to(log_snr_t, ops.shape(xz)[:-1] + (1,)) if time is None: time = self.noise_schedule.time_from_log_snr(log_snr_t, training=training) alpha_t, sigma_t = self.noise_schedule.get_alpha_sigma(log_snr_t=log_snr_t) x_pred = self._predict_x(xz, log_snr_t, alpha_t, sigma_t, conditions, training, masks.subnet_kwargs) score = (alpha_t * x_pred - xz) / ops.square(sigma_t) score = self.guidance_function(x_pred=x_pred, time=time, score=score, **(guidance_kwargs or {})) if not training: score = self.mask_handler.maybe_apply(score, masks.fixed_target_mask) return score
[docs] def velocity( self, xz: Tensor, time: float | Tensor, stochastic_solver: bool, conditions: Tensor = None, training: bool = False, **kwargs, ) -> Tensor: """ Computes the velocity (i.e., time derivative) of the target or latent variable `xz` for either a stochastic differential equation (SDE) or ordinary differential equation (ODE). Parameters ---------- xz : Tensor The current state of the latent variable `z`, typically of shape (..., D), where D is the dimensionality of the latent space. time : float or Tensor Scalar or tensor representing the time (or noise level) at which the velocity should be computed. Will be broadcasted to xz. stochastic_solver : bool If True, computes the velocity for the stochastic formulation (SDE). If False, uses the deterministic formulation (ODE). conditions : Tensor, optional Conditional inputs to the network, such as conditioning variables or encoder outputs. Shape must be broadcastable with `xz`. Default is None. training : bool, optional Whether the model is in training mode. Affects behavior of dropout, batch norm, or other stochastic layers. Default is False. Returns ------- Tensor The velocity tensor of the same shape as `xz`, representing the right-hand side of the SDE or ODE at the given `time`. """ masks = self.mask_handler.resolve( kwargs, targets=xz, conditions=conditions, subnet=self.subnet, ) return self._velocity( xz, time=time, stochastic_solver=stochastic_solver, conditions=conditions, training=training, guidance_kwargs=kwargs.get("guidance_kwargs"), masks=masks, )
def _velocity( self, xz: Tensor, *, time: float | Tensor, stochastic_solver: bool, masks: InferenceMasks, conditions: Tensor = None, training: bool = False, guidance_kwargs: Mapping[str, Any] = None, ) -> Tensor: log_snr_t = expand_right_as(self.noise_schedule.log_snr(t=time, training=training), xz) log_snr_t = ops.broadcast_to(log_snr_t, ops.shape(xz)[:-1] + (1,)) score = self._score( xz, log_snr_t=log_snr_t, conditions=conditions, training=training, guidance_kwargs=guidance_kwargs, masks=masks, ) # compute velocity f, g of the SDE or ODE f, g_squared = self.noise_schedule.get_drift(log_snr_t=log_snr_t, x=xz, training=training) if stochastic_solver: # for the SDE: d(z) = [f(z, t) - g(t) ^ 2 * score(z, lambda )] dt + g(t) dW out = f - g_squared * score else: # for the ODE: d(z) = [f(z, t) - 0.5 * g(t) ^ 2 * score(z, lambda )] dt out = f - 0.5 * g_squared * score # Zero out velocity where target is fixed (during inference only) if not training: out = self.mask_handler.maybe_apply(out, masks.fixed_target_mask) return out
[docs] def diffusion_term( self, xz: Tensor, time: float | Tensor, training: bool = False, **kwargs, ) -> Tensor: """ Compute the diffusion term (standard deviation of the noise) at a given time. Parameters ---------- xz : Tensor Input tensor of shape (..., D), typically representing the target or latent variables at given time. time : float or Tensor The diffusion time step(s). Can be a scalar or a tensor broadcastable to the shape of `xz`. training : bool, optional Whether to use the training noise schedule (default is False). Returns ------- Tensor The diffusion term tensor with shape matching `xz` except for the last dimension, which is set to 1. """ masks = self.mask_handler.resolve(kwargs, targets=xz) return self._diffusion_term(xz, time=time, training=training, masks=masks)
def _diffusion_term( self, xz: Tensor, *, time: float | Tensor, masks: InferenceMasks, training: bool = False, ) -> Tensor: log_snr_t = expand_right_as(self.noise_schedule.log_snr(t=time, training=training), xz) log_snr_t = ops.broadcast_to(log_snr_t, ops.shape(xz)[:-1] + (1,)) g_squared = self.noise_schedule.get_drift(log_snr_t=log_snr_t) g = ops.sqrt(g_squared) # Zero out diffusion where target is fixed (during inference only) if not training: g = self.mask_handler.maybe_apply(g, masks.fixed_target_mask) return g def _compute_grad_of_potential( self, xz: Tensor, norm_log_snr: Tensor, conditions: Tensor = None, training: bool = False, **subnet_kwargs, ) -> Tensor: """ Compute the gradient of the predicted potential phi w.r.t. xz. The network outputs a scalar potential phi(z, t, cond) per sample. The output is then defined as ∇_z phi, i.e. the gradient of the potential w.r.t. the noisy input. Parameters ---------- xz : Tensor The noisy latent variable z at which to evaluate the score. norm_log_snr : Tensor Normalised log-SNR fed to the subnet as the time embedding. conditions : Tensor, optional Conditioning variables passed to the subnet. Default is None. training : bool, optional Whether the subnet is in training mode. **subnet_kwargs Additional keyword arguments forwarded to the subnet (e.g. attention masks). Returns ------- Tensor Output of the same shape as *xz*, equal to some parameterization of ∇_xz phi(xz, t, cond). """ def phi_fn(x): subnet_out = self.subnet( (x, norm_log_snr, conditions), training=training, **subnet_kwargs, ) return self.output_projector(subnet_out) phi, vjp_fn = vjp(phi_fn, xz, return_output=True) return vjp_fn(ops.ones_like(phi)) def _transform_log_snr(self, log_snr: Tensor) -> Tensor: """Transform the log_snr to the range [-1, 1] for the diffusion process.""" log_snr_min = self.noise_schedule.log_snr_min log_snr_max = self.noise_schedule.log_snr_max normalized_snr = (log_snr - log_snr_min) / (log_snr_max - log_snr_min) scaled_value = 2 * normalized_snr - 1 return scaled_value def _integrate( self, xz: Tensor, *, velocity_fn: Callable[[Tensor, Tensor, bool], Tensor], masks: InferenceMasks, integrate_kwargs: dict, seed: keras.random.SeedGenerator, training: bool, density: bool = False, hutchinson_steps: int | None = None, score_fn: Callable[[Tensor, Tensor], Tensor] | None = None, ) -> tuple[Tensor, Tensor | None]: """Integrate ``xz`` with ``velocity_fn(xz, time, stochastic_solver)``. Uses the probability flow ODE for deterministic methods and density computation, and the reverse SDE for stochastic methods. Returns the final state and, if ``density``, the integrated divergence of the velocity. """ method = integrate_kwargs["method"] if density and method in STOCHASTIC_METHODS: logging.warning( "Stochastic methods are not supported for density computation." " Falling back to ODE solver." " Use one of the deterministic methods: " + str(DETERMINISTIC_METHODS) + "." ) integrate_kwargs["method"] = method = "tsit5" if density: # one probe per solve, fixed over time, keeps the augmented ODE deterministic projectors = hutchinson_projectors(xz, hutchinson_steps, seed=seed) def deltas(time, xz): def ode_velocity(x): return velocity_fn(x, time, False) v, trace = jacobian_trace(ode_velocity, xz, projectors=projectors, return_output=True) return {"xz": v, "trace": ops.expand_dims(trace, axis=-1)} state = {"xz": xz, "trace": ops.zeros(ops.shape(xz)[:-1] + (1,), dtype=ops.dtype(xz))} state = integrate(deltas, state, **integrate_kwargs) return state["xz"], ops.squeeze(state["trace"], axis=-1) if method not in STOCHASTIC_METHODS: def deltas(time, xz): return {"xz": velocity_fn(xz, time, False)} state = integrate(deltas, {"xz": xz}, **integrate_kwargs) return state["xz"], None def drift(time, xz): return {"xz": velocity_fn(xz, time, method != "glass")} def diffusion(time, xz): return {"xz": self._diffusion_term(xz, time=time, masks=masks, training=training)} def score(time, xz): return {"xz": score_fn(xz, time)} needs_score = "corrector_steps" in integrate_kwargs or method == "langevin" state_projection_fn, state_update_projection_fn = self.mask_handler.integrator_callbacks(masks) state = integrate_stochastic( drift_fn=drift, diffusion_fn=diffusion, score_fn=score if needs_score else None, noise_schedule=self.noise_schedule, state={"xz": xz}, seed=seed, state_projection_fn=state_projection_fn, state_update_projection_fn=state_update_projection_fn, **integrate_kwargs, ) return state["xz"], None def _forward( self, x: Tensor, conditions: Tensor = None, density: bool = False, training: bool = False, **kwargs, ) -> Tensor | tuple[Tensor, Tensor]: masks = self.mask_handler.resolve( kwargs, targets=x, conditions=conditions, subnet=self.subnet, ) runtime_kwargs = self.mask_handler.without_mask_kwargs(kwargs) guidance_kwargs = runtime_kwargs.pop("guidance_kwargs", None) seed = resolve_seed(runtime_kwargs.pop("seed", None), self.seed_generator) hutchinson_steps = runtime_kwargs.pop("hutchinson_steps", None) # Build integrate kwargs: hardcoded defaults -> instance config -> call-time overrides integrate_kwargs = {"start_time": 0.0, "stop_time": 1.0} | self.integrate_kwargs | runtime_kwargs if integrate_kwargs["method"] in STOCHASTIC_METHODS: logging.warning( "Stochastic methods are not supported for density evaluation." " Falling back to tsit5 ODE solver." " To suppress this warning, explicitly pass a method from " + str(DETERMINISTIC_METHODS) + "." ) integrate_kwargs["method"] = "tsit5" # In a forward pass, fixed coordinates are already present in ``x``. replacement = masks.fixed_target_value if masks.fixed_target_value is not None else x x = self.mask_handler.maybe_apply(x, masks.fixed_target_mask, replacement=replacement) def velocity_fn(xz, time, stochastic_solver): return self._velocity( xz, time=time, stochastic_solver=stochastic_solver, masks=masks, conditions=conditions, training=training, guidance_kwargs=guidance_kwargs, ) z, trace = self._integrate( x, velocity_fn=velocity_fn, masks=masks, integrate_kwargs=integrate_kwargs, seed=seed, training=training, density=density, hutchinson_steps=hutchinson_steps, ) if not density: return z return z, self.base_distribution.log_prob(z, mask=masks.fixed_target_mask) + trace def _inverse( self, z: Tensor, conditions: Tensor = None, density: bool = False, training: bool = False, **kwargs ) -> Tensor | tuple[Tensor, Tensor]: masks = self.mask_handler.resolve( kwargs, targets=z, conditions=conditions, subnet=self.subnet, ) runtime_kwargs = self.mask_handler.without_mask_kwargs(kwargs) guidance_kwargs = runtime_kwargs.pop("guidance_kwargs", None) seed = resolve_seed(runtime_kwargs.pop("seed", None), self.seed_generator) hutchinson_steps = runtime_kwargs.pop("hutchinson_steps", None) # Build integrate kwargs: hardcoded defaults -> instance config -> call-time overrides integrate_kwargs = {"start_time": 1.0, "stop_time": 0.0} | self.integrate_kwargs | runtime_kwargs z = self.mask_handler.maybe_apply(z, masks.fixed_target_mask, masks.fixed_target_value) def velocity_fn(xz, time, stochastic_solver): return self._velocity( xz, time=time, stochastic_solver=stochastic_solver, masks=masks, conditions=conditions, training=training, guidance_kwargs=guidance_kwargs, ) def score_fn(xz, time): return self._score( xz, time=time, masks=masks, conditions=conditions, training=training, guidance_kwargs=guidance_kwargs ) x, trace = self._integrate( z, velocity_fn=velocity_fn, score_fn=score_fn, masks=masks, integrate_kwargs=integrate_kwargs, seed=seed, training=training, density=density, hutchinson_steps=hutchinson_steps, ) if not density: return x return x, self.base_distribution.log_prob(z, mask=masks.fixed_target_mask) - trace
[docs] def compositional_bridge(self, time: Tensor, d0: float = None, d1: float = None) -> Tensor: """Bridge function for compositional diffusion. In the simplest case, this is just 1 if d0 = d1 = 1. Otherwise, it can be used to stabilize the compositional score over time. Parameters ---------- time: Tensor Time step for the diffusion process. d0, d1 : float, optional Bridge values at time 0 and 1. If None, ``compositional_bridge_d0`` and ``compositional_bridge_d1`` are used. Returns ------- Tensor Bridge function value with same shape as time. """ d0 = self.compositional_bridge_d0 if d0 is None else d0 d1 = self.compositional_bridge_d1 if d1 is None else d1 return d0 * ops.exp(-ops.log(d0 / d1) * time)
[docs] def compositional_velocity( self, xz: Tensor, time: float | Tensor, stochastic_solver: bool, conditions: Tensor, seed: keras.random.SeedGenerator, compute_prior_score: Callable[[Tensor], Tensor] = None, mini_batch_size: int | None = None, training: bool = False, **kwargs, ) -> Tensor: """ Computes the compositional velocity for multiple datasets using the formula: s_ψ(θ,t,Y) = (1-n)(1-t) ∇_θ log p(θ) + Σᵢ₌₁ⁿ s_ψ(θ,t,yᵢ) Parameters ---------- xz : Tensor The current state of the latent variable, shape (num_datasets, num_items, ...) time : float or Tensor Time step for the diffusion process stochastic_solver : bool Whether to use stochastic (SDE) or deterministic (ODE) formulation conditions : Tensor Conditional inputs with compositional structure (num_datasets, num_items, ...) seed: SeedGenerator For reproducibility. compute_prior_score: Callable, optional Function to compute the prior score ∇_θ log p(θ). Otherwise, the unconditional score is used. mini_batch_size : int or None Mini batch size for computing individual scores. If None, use all conditions. training : bool, optional Whether in training mode **kwargs Additional keyword arguments passed to the individual score computation. Returns ------- Tensor Compositional velocity of same shape as input xz """ mini_batch_size, masks, repeated_masks = self._resolve_compositional( xz, conditions, mini_batch_size, kwargs.get("use_jac", False) or compute_prior_score is None, kwargs ) return self._compositional_velocity( xz=xz, time=time, stochastic_solver=stochastic_solver, conditions=conditions, compute_prior_score=compute_prior_score, mini_batch_size=mini_batch_size, training=training, seed=seed, masks=masks, repeated_masks=repeated_masks, clip=kwargs.get("clip", (-3, 3)), use_jac=kwargs.get("use_jac", False), guidance_kwargs=kwargs.get("guidance_kwargs"), regularize_precision=kwargs.get("regularize_precision", 1e-6), )
def _resolve_compositional( self, xz: Tensor, conditions: Tensor, mini_batch_size: int | None, with_prior_item: bool, kwargs: dict ) -> tuple[int, InferenceMasks, InferenceMasks]: """Clamp the mini-batch size to the number of items and resolve the masks for compositional scores.""" if conditions is None: raise ValueError("Conditions are required for compositional sampling") num_items = ops.shape(conditions)[1] if mini_batch_size is None or mini_batch_size >= num_items: mini_batch_size = num_items masks = self.mask_handler.resolve_compositional(self.subnet, kwargs, targets=xz, conditions=conditions) repeated_masks = self.mask_handler.repeat_compositional(masks, mini_batch_size + int(with_prior_item)) return mini_batch_size, masks, repeated_masks def _compositional_velocity( self, xz: Tensor, *, time: float | Tensor, stochastic_solver: bool, conditions: Tensor, seed: keras.random.SeedGenerator, masks: InferenceMasks, repeated_masks: InferenceMasks, compute_prior_score: Callable[[Tensor], Tensor] = None, mini_batch_size: int | None = None, training: bool = False, clip: tuple[float, float] | None = (-3, 3), use_jac: bool = False, guidance_kwargs: Mapping[str, Any] = None, regularize_precision: float = 1e-6, bridge: tuple[float | None, float | None] = (None, None), ) -> Tensor: compositional_score = self._compositional_score( xz=xz, time=time, conditions=conditions, compute_prior_score=compute_prior_score, mini_batch_size=mini_batch_size, training=training, seed=seed, masks=masks, repeated_masks=repeated_masks, clip=clip, use_jac=use_jac, guidance_kwargs=guidance_kwargs, regularize_precision=regularize_precision, bridge=bridge, ) # Calculate standard noise schedule components log_snr_t = expand_right_as(self.noise_schedule.log_snr(t=time, training=training), xz) log_snr_t = ops.broadcast_to(log_snr_t, ops.shape(xz)[:-1] + (1,)) # Compute velocity using standard drift-diffusion formulation f, g_squared = self.noise_schedule.get_drift(log_snr_t=log_snr_t, x=xz, training=training) if stochastic_solver: # for the SDE: d(z) = [f(z, t) - g(t) ^ 2 * score(z, lambda )] dt + g(t) dW out = f - g_squared * compositional_score else: # for the ODE: d(z) = [f(z, t) - 0.5 * g(t) ^ 2 * score(z, lambda )] dt out = f - 0.5 * g_squared * compositional_score # Zero out velocity where target is fixed (during inference only) if not training: out = self.mask_handler.maybe_apply(out, masks.fixed_target_mask) return out
[docs] def compositional_score( self, xz: Tensor, time: float | Tensor, conditions: Tensor, seed: keras.random.SeedGenerator | None, compute_prior_score: Callable[[Tensor], Tensor] = None, mini_batch_size: int = None, training: bool = False, clip: tuple[float, float] | None = (-3, 3), use_jac: bool = False, guidance_kwargs: Mapping[str, Any] = None, **kwargs, ) -> Tensor: """ Computes the compositional score for multiple datasets. Parameters ---------- xz : Tensor The current state of the latent variable, shape (num_datasets, num_items, ...) time : float or Tensor Time step for the diffusion process conditions : Tensor Conditional inputs with compositional structure (num_datasets, num_items, ...) seed: keras.random.SeedGenerator or None Optional seed for reproducibility. compute_prior_score: Callable, optional Function to compute the prior score ∇_θ log p(θ). Otherwise, the unconditional score is used. mini_batch_size : int or None Mini batch size for computing individual scores. If None, use all conditions. training : bool, optional Whether in training mode clip: (float, float), optional Whether to clip the predicted x for numerical stability at given values. use_jac: bool, optional Whether to use the Jacobian-based compositional score instead of the direct sum. guidance_kwargs : Mapping[str, Any], optional A dictionary of parameters for computing a guidance constraint term, which is added to the score for guided sampling. The specific keys and values depend on the implementation of `guidance_function`. **kwargs Additional keyword arguments passed to the individual score computation Returns ------- Tensor Compositional velocity of same shape as input xz """ mini_batch_size, masks, repeated_masks = self._resolve_compositional( xz, conditions, mini_batch_size, use_jac or compute_prior_score is None, kwargs ) return self._compositional_score( xz=xz, time=time, conditions=conditions, seed=seed, compute_prior_score=compute_prior_score, mini_batch_size=mini_batch_size, training=training, clip=clip, use_jac=use_jac, guidance_kwargs=guidance_kwargs, masks=masks, repeated_masks=repeated_masks, regularize_precision=kwargs.get("regularize_precision", 1e-6), )
def _compositional_score( self, xz: Tensor, *, time: float | Tensor, conditions: Tensor, seed: keras.random.SeedGenerator | None, masks: InferenceMasks, repeated_masks: InferenceMasks, compute_prior_score: Callable[[Tensor], Tensor] = None, mini_batch_size: int = None, training: bool = False, clip: tuple[float, float] | None = (-3, 3), use_jac: bool = False, guidance_kwargs: Mapping[str, Any] = None, regularize_precision: float = 1e-6, bridge: tuple[float | None, float | None] = (None, None), ) -> Tensor: log_snr_t = expand_right_as(self.noise_schedule.log_snr(t=time, training=training), xz) log_snr_t = ops.broadcast_to(log_snr_t, ops.shape(xz)[:-1] + (1,)) alpha_t, sigma_t = self.noise_schedule.get_alpha_sigma(log_snr_t=log_snr_t) time = ops.cast(time, dtype=ops.dtype(xz)) if not use_jac: compositional_score = self._compositional_score_direct( xz=xz, time=time, log_snr_t=log_snr_t, conditions=conditions, compute_prior_score=compute_prior_score, mini_batch_size=mini_batch_size, training=training, seed=seed, masks=masks, repeated_masks=repeated_masks, ) else: # uses a Gaussian approximation, can be very slow compositional_score = self._compositional_score_jac( xz=xz, time=time, log_snr_t=log_snr_t, sigma_t=sigma_t, conditions=conditions, compute_prior_score=compute_prior_score, mini_batch_size=mini_batch_size, training=training, seed=seed, masks=masks, repeated_masks=repeated_masks, regularize_precision=regularize_precision, ) compositional_score = self.compositional_bridge(time, *bridge) * compositional_score x_pred = (xz + sigma_t**2 * compositional_score) / alpha_t compositional_score = self.guidance_function( x_pred=x_pred, time=time, score=compositional_score, **(guidance_kwargs or {}) ) compositional_score = self._maybe_clip_score(compositional_score, clip, alpha_t, sigma_t, xz) return self.mask_handler.maybe_apply(compositional_score, masks.fixed_target_mask) def _compositional_score_direct( self, xz: Tensor, time: float | Tensor, log_snr_t: Tensor, conditions: Tensor, *, masks: InferenceMasks, repeated_masks: InferenceMasks, seed: keras.random.SeedGenerator = None, compute_prior_score: Callable[[Tensor], Tensor] = None, mini_batch_size: int = None, training: bool = False, ) -> Tensor: """ Computes the compositional score for multiple datasets using the formula: s_ψ(θ,t,Y) = (1-n)(1-t) ∇_θ log p(θ) + Σᵢ₌₁ⁿ s_ψ(θ,t,yᵢ) with possible weighting of the scores. Parameters ---------- xz : Tensor The current state of the latent variable, shape (num_datasets, num_items, ...) time : float or Tensor Time step for the diffusion process. log_snr_t : Tensor Log SNR at time t, broadcastable to shape of xz. conditions : Tensor Conditional inputs with compositional structure (num_datasets, num_items, ...) seed: keras.random.SeedGenerator or None Optional seed for reproducibility. compute_prior_score: Callable, optional Function to compute the prior score ∇_θ log p(θ). Otherwise, the unconditional score is estimated. mini_batch_size : int or None Mini batch size for computing individual scores. If None, use all conditions. training : bool, optional Whether in training mode. **kwargs Additional keyword arguments passed to the individual score computation. Returns ------- Tensor Compositional score of same shape as input xz """ batch_size, num_items = ops.shape(conditions)[:2] if mini_batch_size is None or mini_batch_size > num_items: mini_batch_size = num_items needs_network_prior = compute_prior_score is None num_total = mini_batch_size + int(needs_network_prior) xz_flat, log_snr_flat, conditions_flat, score_masks = self._flatten_compositional_items( xz, log_snr_t, conditions, masks, repeated_masks, mini_batch_size, seed, with_prior=needs_network_prior ) scores_flat = self._score( xz_flat, log_snr_t=log_snr_flat, conditions=conditions_flat, training=training, masks=score_masks, ) all_scores = ops.reshape(scores_flat, (batch_size, num_total) + tuple(ops.shape(xz)[1:])) individual_scores = all_scores[:, :mini_batch_size] if needs_network_prior: prior_score = all_scores[:, -1] else: # internally uses a (1-time) weight if prior score has no time argument prior_score = compute_prior_score(xz, time) # Combined score using compositional formula: (1-n) prior_score + Σᵢ₌₁ⁿ posterior_score delta = individual_scores - ops.expand_dims(prior_score, axis=1) compositional_score = prior_score + (num_items / mini_batch_size) * ops.sum(delta, axis=1) return compositional_score def _compositional_score_jac( self, xz: Tensor, time: float | Tensor, log_snr_t: Tensor, sigma_t: Tensor, conditions: Tensor, *, masks: InferenceMasks, repeated_masks: InferenceMasks, seed: keras.random.SeedGenerator = None, compute_prior_score: Callable[[Tensor], Tensor] = None, mini_batch_size: int = None, training: bool = False, regularize_precision: float = 1e-6, ) -> Tensor: """Stabilized version of JAC compositional score (Linhart et al. 2026) with mini-batching from (Arruda et al. 2026). Precision-weighted compositional posterior score where both individual posterior precisions and the prior precision are estimated via the Jacobian of the score network. Parameters ---------- xz : Tensor The current state of the latent variable, shape (num_datasets, num_items, ...) time : float or Tensor Time step for the diffusion process. log_snr_t : Tensor Log SNR at time t, broadcastable to shape of xz. sigma_t : Tensor Sigma component of noise schedule at time t, broadcastable to shape of xz. conditions : Tensor Conditional inputs with compositional structure (num_datasets, num_items, ...) seed: SeedGenerator or None Optional seed for reproducibility. compute_prior_score: Callable, optional Function to compute the prior score ∇_θ log p(θ). Otherwise, the unconditional score is used. mini_batch_size : int or None, optional Mini batch size for computing individual scores. If None, use all conditions. training : bool Whether in training mode. regularize_precision : float Tikhonov regularization added to Λ before solving for numerical stability. **kwargs Additional keyword arguments passed to the individual score computation. Returns ------- Tensor Compositional score of same shape as input xz """ batch_size, num_items = ops.shape(conditions)[:2] m = ops.shape(xz)[-1] upsilon_t = ops.maximum(ops.reshape(sigma_t, (-1,))[0] ** 2, 1e-8) if mini_batch_size is None or mini_batch_size > num_items: mini_batch_size = num_items num_total = mini_batch_size + 1 # the prior is appended as an extra, zero-conditioned observation xz_rep, lsnr_rep, cond_flat, score_masks = self._flatten_compositional_items( xz, log_snr_t, conditions, masks, repeated_masks, mini_batch_size, seed, with_prior=True ) all_scores, all_jacs = self._compute_score_and_jacobian( xz=xz_rep, log_snr_t=lsnr_rep, conditions=cond_flat, training=training, masks=score_masks, ) # Reshape: (B, num_total, m) and (B, num_total, m, m) all_scores = ops.reshape(all_scores, (batch_size, num_total, m)) all_jacs = ops.reshape(all_jacs, (batch_size, num_total, m, m)) # compute P_k = (I + υ J_k)⁻¹ for all k at once I_m = ops.eye(m, dtype=ops.dtype(xz)) I_4d = ops.broadcast_to( ops.reshape(I_m, (1, 1, m, m)), (batch_size, num_total, m, m), ) A_all = I_4d + upsilon_t * all_jacs P_all = ops.inv(A_all) # P_k @ s_k for all k: (B, num_total, m, 1) → (B, num_total, m) scores_col = ops.expand_dims(all_scores, -1) Ps_all = ops.squeeze(ops.matmul(P_all, scores_col), axis=-1) # (B, num_total, m) # split observations and prior P_obs = P_all[:, :mini_batch_size] # (B, n_obs, m, m) Ps_obs = Ps_all[:, :mini_batch_size] # (B, n_obs, m) P_lambda = P_all[:, -1] # (B, m, m) # Sum over observations sum_P = ops.sum(P_obs, axis=1) # (B, m, m) sum_Ps = ops.sum(Ps_obs, axis=1) # (B, m) # Scale for mini-batch estimator if mini_batch_size < num_items: scale = num_items / mini_batch_size sum_P = scale * sum_P sum_Ps = scale * sum_Ps # prior score if compute_prior_score is not None: s_lambda = compute_prior_score(xz, time) P_lambda_s = ops.squeeze(ops.matmul(P_lambda, ops.expand_dims(s_lambda, -1)), axis=-1) else: P_lambda_s = Ps_all[:, -1] # already computed # solve: Λ x = s̃ w_prior = 1 - num_items I_m_batch = ops.broadcast_to(I_m, (batch_size, m, m)) lambda_matrix = sum_P + w_prior * P_lambda + regularize_precision * I_m_batch s_tilde = sum_Ps + w_prior * P_lambda_s compositional_score = linsolve_batched(lambda_matrix, s_tilde) return compositional_score def _flatten_compositional_items( self, xz: Tensor, log_snr_t: Tensor, conditions: Tensor, masks: InferenceMasks, repeated_masks: InferenceMasks, mini_batch_size: int, seed: keras.random.SeedGenerator | None, with_prior: bool, ) -> tuple[Tensor, Tensor, Tensor, InferenceMasks]: """Flatten a random mini-batch of the items of each dataset into the batch axis for one score evaluation. With ``with_prior``, a zero condition is appended as last item, whose score estimates the prior score. """ batch_size, num_items = ops.shape(conditions)[:2] observed_condition_mask = masks.observed_condition_mask if mini_batch_size < num_items: ranks = keras.random.uniform((batch_size, num_items), seed=seed) _, per_row_idx = ops.top_k(-ranks, mini_batch_size) conditions = ops.take_along_axis(conditions, per_row_idx[..., None], axis=1) if observed_condition_mask is not None: observed_condition_mask = ops.take_along_axis(observed_condition_mask, per_row_idx[..., None], axis=1) if with_prior: conditions = ops.concatenate([conditions, ops.zeros_like(conditions[:, :1])], axis=1) if observed_condition_mask is not None: observed_condition_mask = ops.concatenate( [observed_condition_mask, ops.zeros_like(observed_condition_mask[:, :1])], axis=1 ) num_total = mini_batch_size + int(with_prior) conditions_flat = ops.reshape(conditions, (batch_size * num_total,) + tuple(ops.shape(conditions)[2:])) return ( repeat_and_flatten(xz, num_total), repeat_and_flatten(log_snr_t, num_total), conditions_flat, self.mask_handler.with_flattened_condition_mask(repeated_masks, observed_condition_mask), ) def _compute_score_and_jacobian( self, xz: Tensor, log_snr_t: Tensor, conditions: Tensor, masks: InferenceMasks, training: bool = False, ) -> tuple[Tensor, Tensor]: """Compute score and JAC precision Σ̂⁻¹ for one conditioning input using full Jacobian J = ∇_{θ_t} s(θ_t). Returns ------- s : Tensor (B, m) prec : Tensor (B, m, m) Σ̂⁻¹ = (α_t/υ_t)(I + υ_t J)⁻¹ """ m = ops.shape(xz)[-1] def score_fn(x): return self._score( xz=x, log_snr_t=log_snr_t, conditions=conditions, training=training, masks=masks, ) score, vjp_fn = vjp(score_fn, xz, return_output=True) rows = [] for i in range(m): e_i = ops.broadcast_to( ops.reshape(keras.ops.one_hot(i, num_classes=m, dtype=ops.dtype(score)), (1, m)), (ops.shape(score)[0], m), ) row_i = vjp_fn(e_i) rows.append(ops.expand_dims(row_i, axis=1)) jacobian = ops.concatenate(rows, axis=1) return score, jacobian @staticmethod def _maybe_clip_score(compositional_score, clip, alpha_t, sigma_t, xz): if clip is not None: min_clip, max_clip = clip x_pred = (xz + sigma_t**2 * compositional_score) / alpha_t x_pred = ops.clip(x_pred, min_clip, max_clip) compositional_score = (x_pred * alpha_t - xz) / (sigma_t**2) return compositional_score def _inverse_compositional( self, z: Tensor, conditions: Tensor, compute_prior_score: Callable[[Tensor], Tensor] = None, density: bool = False, training: bool = False, **kwargs, ) -> Tensor | tuple[Tensor, Tensor]: """ Inverse pass for compositional diffusion sampling. """ runtime_kwargs = self.mask_handler.without_mask_kwargs(kwargs) seed = resolve_seed(runtime_kwargs.pop("seed", None), self.seed_generator) hutchinson_steps = runtime_kwargs.pop("hutchinson_steps", None) # Build integrate kwargs: hardcoded defaults -> instance config -> call-time overrides integrate_kwargs = {"start_time": 1.0, "stop_time": 0.0} | self.integrate_kwargs | runtime_kwargs num_items = ops.shape(conditions)[1] mini_batch_size = integrate_kwargs.pop("mini_batch_size", max(num_items * 0.1, 2)) mini_batch_size = num_items if mini_batch_size is None else min(max(int(mini_batch_size), 1), num_items) if keras.backend.backend() == "jax" and mini_batch_size != num_items: mini_batch_size = num_items logging.warning("Setting mini_batch_size to num_items as jax does not support mini-batching.") score_kwargs = { "clip": integrate_kwargs.pop("clip", (-3, 3)), "use_jac": integrate_kwargs.pop("use_jac", False), "guidance_kwargs": integrate_kwargs.pop("guidance_kwargs", None), "regularize_precision": integrate_kwargs.pop("regularize_precision", 1e-6), # per-call bridge values, the instance defaults stay untouched "bridge": ( float(integrate_kwargs.pop("compositional_bridge_d0", self.compositional_bridge_d0)), float(integrate_kwargs.pop("compositional_bridge_d1", self.compositional_bridge_d1)), ), } _, masks, repeated_masks = self._resolve_compositional( z, conditions, mini_batch_size, score_kwargs["use_jac"] or compute_prior_score is None, kwargs ) if integrate_kwargs["method"] == "langevin": # Geffner et al. (2023) z_scaling = num_items * score_kwargs["bridge"][1] z = z / ops.sqrt(ops.cast(z_scaling, dtype=ops.dtype(z))) z = self.mask_handler.maybe_apply(z, masks.fixed_target_mask, masks.fixed_target_value) def velocity_fn(xz, time, stochastic_solver): return self._compositional_velocity( xz, time=time, stochastic_solver=stochastic_solver, conditions=conditions, compute_prior_score=compute_prior_score, mini_batch_size=mini_batch_size, training=training, seed=seed, masks=masks, repeated_masks=repeated_masks, **score_kwargs, ) def score_fn(xz, time): return self._compositional_score( xz=xz, time=time, conditions=conditions, compute_prior_score=compute_prior_score, mini_batch_size=mini_batch_size, training=training, seed=seed, masks=masks, repeated_masks=repeated_masks, **score_kwargs, ) x, trace = self._integrate( z, velocity_fn=velocity_fn, score_fn=score_fn, masks=masks, integrate_kwargs=integrate_kwargs, seed=seed, training=training, density=density, hutchinson_steps=hutchinson_steps, ) if not density: return x return x, self.base_distribution.log_prob(z, mask=masks.fixed_target_mask) - trace