Source code for scimba_torch.integration.monte_carlo_parameters

"""Parameter samplers for Monte Carlo simulations."""

from typing import Any, Sequence, cast

import torch

from scimba_torch.domain.meshless_domain.domain_1d import Segment1D
from scimba_torch.domain.meshless_domain.domain_2d import Circle2D, Square2D
from scimba_torch.domain.meshless_domain.domain_3d import Cube3D
from scimba_torch.utils.scimba_tensors import LabelTensor

PARAM_TYPE = int | float


def _is_param_type(arg: Any) -> bool:
    return isinstance(arg, int) or isinstance(arg, float)


def _is_param_domain_type(arg: Any) -> bool:
    return isinstance(arg, Sequence) and all(
        isinstance(a, Sequence)
        and (len(a) == 2)
        and _is_param_type(a[0])
        and _is_param_type(a[0])
        for a in arg
    )


def _check_and_cast_argument(bounds: Any) -> tuple[bool, list[tuple[float, float]]]:
    resT = []
    resB = _is_param_domain_type(bounds)
    if resB:
        arg = cast(Sequence, bounds)
        for a in arg:
            resT.append((float(a[0]), float(a[1])))

    return resB, resT


[docs] class UniformParametricSampler: """Sample uniformly from given bounds for each dimension. Args: bounds: A list of tuples where each tuple contains the lower and upper bounds for each dimension. Raises: TypeError: If parameters domain is not a list of tuples of two floats. ValueError: If any bound has lower value greater than upper value. """ def __init__(self, bounds: list[tuple[float, float]]): check, nbounds = _check_and_cast_argument(bounds) if not check: raise TypeError("parameters domain must be a list of tuples of two floats") if not all(bound[0] <= bound[1] for bound in nbounds): raise ValueError( "can not create a time sampler for empty time domain " + str(nbounds) ) #: A list of tuples where each tuple contains the lower and upper bounds #: for each dimension. self.bounds = nbounds self.dim = len( self.bounds ) #: The number of dimensions, inferred from the length of bounds.
[docs] def sample(self, n: int) -> LabelTensor: """Generates samples uniformly within the specified bounds for each dimension. Args: n: The number of samples to generate. Returns: A tensor containing the generated samples and corresponding labels. Raises: TypeError: If argument is not an integer. ValueError: If argument is negative. """ if not isinstance(n, int): raise TypeError("argument to sample method must be an integer") if n < 0: raise ValueError("argument to sample method must be non-negative") samples = torch.rand(n, self.dim) for i in range(self.dim): samples[:, i] = ( samples[:, i] * (self.bounds[i][1] - self.bounds[i][0]) + self.bounds[i][0] ) samples.requires_grad_() labels = torch.zeros(n, dtype=torch.int32) data = LabelTensor(samples, labels) return data
[docs] class UniformVelocitySampler: """Sample uniformly the velocities in a circle of radius r. Args: velocity_domain: Velocity domain in which the velocity will be drawn. Raises: TypeError: If velocity domain is not an object of class Circle2D. """ def __init__(self, velocity_domain: Circle2D): if not isinstance(velocity_domain, Circle2D): raise TypeError("velocity domain must be an object of class Circle2D") self.velocity_domain = ( velocity_domain #: Velocity domain in which the velocity will be drawn. ) self.dim = velocity_domain.dim #: The number of dimensions, here equal to 2.
[docs] def sample(self, n: int) -> LabelTensor: """Generates samples uniformly within the specified bounds for each dimension. Args: n: The number of samples to generate. Returns: A tensor containing the generated samples and corresponding labels. Raises: TypeError: If argument is not an integer. ValueError: If argument is negative. """ if not isinstance(n, int): raise TypeError("argument to sample method must be an integer") if n < 0: raise ValueError("argument to sample method must be non-negative") theta = torch.rand(n) * 2 * torch.pi - torch.pi samples = torch.zeros(n, self.dim) samples[:, 0] = self.velocity_domain.radius * torch.cos(theta[:]) samples[:, 1] = self.velocity_domain.radius * torch.sin(theta[:]) samples.requires_grad_() labels = torch.zeros(n, dtype=torch.int32) data = LabelTensor(samples, labels) return data
[docs] class UniformVelocitySamplerOnCuboid: """Sample uniformly the velocities in a cuboid. Args: velocity_domain: Velocity domain in which the velocity will be drawn. Raises: TypeError: If velocity domain is not an object of class Segment1D, Square2D or Cube3D. """ def __init__(self, velocity_domain: Segment1D | Square2D | Cube3D): if not isinstance(velocity_domain, (Segment1D, Square2D, Cube3D)): raise TypeError( "velocity domain must be an object of class Segment1D, Square2D or " f"Cube3D, got {type(velocity_domain)}" ) self.velocity_domain = ( velocity_domain #: Velocity domain in which the velocity will be drawn. ) self.dim = velocity_domain.dim #: The number of dimensions. self.domain_size = ( self.velocity_domain.bounds[:, 1] - self.velocity_domain.bounds[:, 0] ) #: The size of the domain in each dimension. self.lower_bound = self.velocity_domain.bounds[ :, 0 ] #: The lower bound of the domain.
[docs] def sample(self, n: int) -> LabelTensor: """Generates samples uniformly within the specified bounds for each dimension. Args: n: The number of samples to generate. Returns: A tensor containing the generated samples and corresponding labels. Raises: TypeError: If argument is not an integer. ValueError: If argument is negative. """ if not isinstance(n, int): raise TypeError("argument to sample method must be an integer") if n < 0: raise ValueError("argument to sample method must be non-negative") if self.dim == 1: random = torch.rand(n, requires_grad=True) else: random = torch.rand(n, self.dim, requires_grad=True) samples = random * self.domain_size + self.lower_bound if self.dim == 1: samples.unsqueeze_(1) labels = torch.zeros(n, dtype=torch.int32) return LabelTensor(samples, labels)