Source code for pina.domain.cartesian_domain

"""Module for the Cartesian Domain."""

import torch
from .base_domain import BaseDomain
from .union import Union
from ..utils import torch_lhs, chebyshev_roots, check_consistency
from ..label_tensor import LabelTensor


[docs] class CartesianDomain(BaseDomain): """ Implementation of the hypercube domain, obtained as the cartesian product of one-dimensional intervals. :Example: >>> cartesian_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) >>> cartesian_domain = CartesianDomain({'x': [0, 1], 'y': 1.0}) """ def __init__(self, cartesian_dict): """ Initialization of the :class:`CartesianDomain` class. :param dict cartesian_dict: A dictionary where the keys are the variable names and the values are the domain extrema. The domain extrema can be either a list or tuple with two elements or a single number. If the domain extrema is a single number, the variable is fixed to that value. :raises TypeError: If the cartesian dictionary is not a dictionary. :raises ValueError: If the cartesian dictionary contains variables with invalid ranges. :raises ValueError: If the cartesian dictionary contains values that are neither numbers nor lists/tuples of numbers of length 2. """ # Initialization super().__init__(variables_dict=cartesian_dict) self._sample_modes = ("random", "grid", "chebyshev", "lh", "latin")
[docs] def is_inside(self, point, check_border=False): """ Check if a point is inside the domain. :param LabelTensor point: The point to check. :param bool check_border: If ``True``, the boundary is considered inside the domain. Default is ``False``. :raises ValueError: If ``point`` is not a :class:`LabelTensor`. :raises ValueError: If the labels of ``point`` differ from the variables of the domain. :return: Whether the point is inside the domain or not. :rtype: bool """ # Checks on point check_consistency(point, LabelTensor) if set(self.variables) != set(point.labels): raise ValueError( "Point labels differ from domain's dictionary labels. " f"Got {sorted(point.labels)}, expected {self.variables}." ) # Fixed variable checks fixed_check = all( (point.extract([k]) == v).all() for k, v in self._fixed.items() ) # If there are no range variables, return fixed variable check if not self._range: return fixed_check # Ranged variable checks -- check_border True if check_border: range_check = all( ( (point.extract([k]) >= low) & (point.extract([k]) <= high) ).all() for k, (low, high) in self._range.items() ) # Ranged variable checks -- check_border False else: range_check = all( ((point.extract([k]) > low) & (point.extract([k]) < high)).all() for k, (low, high) in self._range.items() ) return fixed_check and range_check
[docs] def sample(self, n, mode="random", variables="all"): """ The sampling routine. :param int n: The number of samples to generate. See Note for reference. :param str mode: The sampling method. Available modes: ``random`` for random sampling; ``latin`` or ``lh`` for latin hypercube sampling; ``chebyshev`` for chebyshev sampling; ``grid`` for grid sampling. Default is ``random``. :param variables: The list of variables to sample. If ``all``, all variables are sampled. Default is ``all``. :type variables: list[str] | str :raises AssertionError: If ``n`` is not a positive integer. :raises ValueError: If the sampling mode is invalid. :raises ValueError: If ``variables`` is neither ``all``, a string, nor a list/tuple of strings. :raises ValueError: If any of the specified variables is unknown. :return: The sampled points. :rtype: LabelTensor .. note:: When multiple variables are involved, the total number of sampled points may differ depending on the chosen ``mode``. If ``mode`` is ``grid`` or ``chebyshev``, points are sampled independently for each variable and then combined, resulting in a total number of points equal to ``n`` raised to the power of the number of variables. If ``mode`` is ``random``, ``lh`` or ``latin``, all variables are sampled together, and the total number of points remains ``n``. .. warning:: The extrema of CartesianDomain are only sampled when using the ``grid`` mode. :Example: >>> cartesian_domain = CartesianDomain({'x': [0, 1], 'y': [0, 1]}) >>> cartesian_domain.sample(n=3, mode='random') LabelTensor([[0.0108, 0.7643], [0.4477, 0.8015], [0.8735, 0.6349]]) >>> cartesian_domain.sample(n=3, mode='grid') LabelTensor([[0.0000, 0.0000], [0.5000, 0.0000], [1.0000, 0.0000], [0.0000, 0.5000], [0.5000, 0.5000], [1.0000, 0.5000], [0.0000, 1.0000], [0.5000, 1.0000], [1.0000, 1.0000]]) """ # Validate sampling settings variables = self._validate_sampling(n, mode, variables) # Separate range and fixed variables range_vars = [v for v in variables if v in self._range] fixed_vars = [v for v in variables if v in self._fixed] # If there are no range variables, return fixed variables only if not range_vars: vals = [torch.full((n, 1), self._fixed[v]) for v in fixed_vars] result = torch.cat(vals, dim=1) result = result.as_subclass(LabelTensor) result.labels = fixed_vars return result # Create a tensor of bounds for the range variables bounds = torch.as_tensor([self._range[v] for v in range_vars]) # Sample for mode random or latin hypercube if mode in {"random", "lh", "latin"}: pts = self._sample_range(n, mode, bounds) # Sample for mode grid or chebyshev else: grids = [ self._sample_range( n, mode, torch.as_tensor([self._range[v]]) ).reshape(-1) for v in range_vars ] pts = torch.cartesian_prod(*grids).reshape(-1, len(grids)) # Add fixed vars if fixed_vars: fixed_vals = [ torch.full((pts.shape[0], 1), self._fixed[v]) for v in fixed_vars ] pts = torch.cat([pts] + fixed_vals, dim=1) labels = range_vars + fixed_vars else: labels = range_vars # Create the result as a LabelTensor pts = pts.as_subclass(LabelTensor) pts.labels = labels return pts[sorted(pts.labels)]
def _sample_range(self, n, mode, bounds): """ Sample points and rescale to fit within the specified bounds. :param int n: The number of points to sample. :param str mode: The sampling method. Default is ``random``. :param torch.Tensor bounds: The bounds of the domain. :return: The rescaled sample points. :rtype: torch.Tensor """ # Define a dictionary of sampling methods samplers = { "random": lambda: torch.rand(size=(n, bounds.shape[0])), "chebyshev": lambda: chebyshev_roots(n) .mul(0.5) .add(0.5) .reshape(-1, 1), "grid": lambda: torch.linspace(0, 1, n).reshape(-1, 1), "lh": lambda: torch_lhs(n, bounds.shape[0]), "latin": lambda: torch_lhs(n, bounds.shape[0]), } # Sample points in [0, 1]^d and rescale to the desired bounds pts = samplers[mode]() return pts * (bounds[:, 1] - bounds[:, 0]) + bounds[:, 0]
[docs] def partial(self): """ Return the boundary of the domain as a :class:`Union` object. :return: The boundary of the domain. :rtype: Union """ faces = [] # Iterate over ranged variables for var, (low, high) in self._range.items(): # Fix the variable to its low value to get the lower face lower = CartesianDomain({**self._fixed, **self._range, var: low}) # Fix the variable to its high value to get the upper face higher = CartesianDomain({**self._fixed, **self._range, var: high}) faces.extend([lower, higher]) return Union(faces)