Skip to content

PyTorch backend

Install the optional torch extra to run this API. Read the PyTorch guide for an example with backpropagation.

compute accepts a TensorLandscapeSample, feature selectors, y_normalization, and options. It returns the same result model as the NumPy backend, with scalar tensors as feature values. This backend has no rng or workers parameter.

list_features() returns specifications for the Torch profile only. list_capabilities() reports device, dtype, and autograd support including the requested preprocessing mode. Known features without a Torch implementation raise UnsupportedFeatureError; unavailable devices raise UnsupportedFeatureDeviceError.

compute

compute(
    sample: TensorLandscapeSample,
    features: str | tuple[str, ...] | list[str],
    *,
    y_normalization: YNormalization = "minmax",
    options: FeatureOptions | None = None,
) -> ComputationResult[torch.Tensor]

Compute tensor features with min-max objective normalization by default.

y_normalization=None preserves raw canonical objectives. Min-max preprocessing is piecewise differentiable. All modes preserve the sample's tensors, dtype, device, and autograd history. options is a nested mapping keyed by feature group, for example {"fitness_distance": {"proportion_of_best": 0.25}}. Omit it to use defaults. Unknown groups and option names are rejected. Fitness-distance features always use Euclidean distances to the best selected observation. Effective options are recorded in result.metadata.options as an immutable snapshot.

Source code in src/orivex/torch/api.py
def compute(
    sample: TensorLandscapeSample,
    features: str | tuple[str, ...] | list[str],
    *,
    y_normalization: YNormalization = "minmax",
    options: FeatureOptions | None = None,
) -> ComputationResult[torch.Tensor]:
    """Compute tensor features with min-max objective normalization by default.

    ``y_normalization=None`` preserves raw canonical objectives.
    Min-max preprocessing is piecewise differentiable.
    All modes preserve the sample's tensors, dtype, device, and autograd history.
    ``options`` is a nested mapping keyed by feature group, for example
    ``{"fitness_distance": {"proportion_of_best": 0.25}}``. Omit it to use defaults.
    Unknown groups and option names are rejected. Fitness-distance features always use
    Euclidean distances to the best selected observation. Effective options are recorded
    in ``result.metadata.options`` as an immutable snapshot.
    """

    if not isinstance(sample, TensorLandscapeSample):
        raise TypeError("sample must be a orivex.torch.TensorLandscapeSample")
    feature_names = _supported_feature_names(features)
    capabilities = {capability.feature_name: capability for capability in CAPABILITIES}
    unavailable = tuple(
        name for name in feature_names if sample.device_type not in capabilities[name].devices
    )
    if unavailable:
        joined = ", ".join(unavailable)
        raise UnsupportedFeatureDeviceError(
            f"features are not available on device type {sample.device_type!r}: {joined}"
        )
    return DEFAULT_ENGINE.compute(
        sample,
        feature_names,
        y_normalization=y_normalization,
        options=options,
    )

TensorLandscapeSample dataclass

A validated single-landscape tensor sample with mutation detection.

Source code in src/orivex/torch/sample.py
@dataclass(frozen=True, slots=True, init=False)
class TensorLandscapeSample:
    """A validated single-landscape tensor sample with mutation detection."""

    x: torch.Tensor
    y: torch.Tensor
    lower: torch.Tensor
    upper: torch.Tensor
    sense: ObjectiveSense
    _minimization_y: torch.Tensor
    _fingerprint: str
    _versions: tuple[int, ...]

    def __init__(
        self,
        x: torch.Tensor,
        y: torch.Tensor,
        lower: TensorValue,
        upper: TensorValue,
        sense: ObjectiveSense | ObjectiveSenseName = ObjectiveSense.MINIMIZE,
    ) -> None:
        if not isinstance(x, torch.Tensor) or not isinstance(y, torch.Tensor):
            raise TypeError("X and y must be torch tensors")
        if x.dtype not in _SUPPORTED_DTYPES or y.dtype not in _SUPPORTED_DTYPES:
            raise TypeError("X and y must have dtype torch.float32 or torch.float64")
        if x.dtype != y.dtype:
            raise ValueError("X and y must have the same dtype")
        if x.device != y.device:
            raise ValueError("X and y must be on the same device")
        if x.device.type not in _SUPPORTED_DEVICES:
            raise ValueError(f"unsupported device type: {x.device.type!r}")

        x_array = x.clone()
        y_array = y.clone()
        lower_array = _clone_bound(lower, reference=x_array, name="lower")
        upper_array = _clone_bound(upper, reference=x_array, name="upper")
        sense_value = ObjectiveSense(sense)

        if x_array.ndim != 2:
            raise ValueError(f"X must be 2-dimensional, got shape {tuple(x_array.shape)}")
        if y_array.ndim != 1:
            raise ValueError(f"y must be 1-dimensional, got shape {tuple(y_array.shape)}")
        observations, dimension = x_array.shape
        if observations == 0 or dimension == 0:
            raise ValueError("X must contain at least one observation and one variable")
        if y_array.shape != (observations,):
            raise ValueError(f"y must have shape ({observations},), got {tuple(y_array.shape)}")
        if lower_array.shape != (dimension,) or upper_array.shape != (dimension,):
            raise ValueError(f"bounds must both have shape ({dimension},)")
        if not bool(torch.isfinite(x_array).all().item()):
            raise ValueError("X must contain only finite values")
        if not bool(torch.isfinite(y_array).all().item()):
            raise ValueError("y must contain only finite values")
        if not bool(torch.isfinite(lower_array).all().item()):
            raise ValueError("lower must contain only finite values")
        if not bool(torch.isfinite(upper_array).all().item()):
            raise ValueError("upper must contain only finite values")
        if not bool((lower_array < upper_array).all().item()):
            raise ValueError("every lower bound must be strictly smaller than its upper bound")
        inside = (x_array >= lower_array) & (x_array <= upper_array)
        if not bool(inside.all().item()):
            raise ValueError("all observations must lie within the inclusive box bounds")

        minimization_y = y_array if sense_value is ObjectiveSense.MINIMIZE else -y_array
        tensors = (x_array, y_array, lower_array, upper_array, minimization_y)
        object.__setattr__(self, "x", x_array)
        object.__setattr__(self, "y", y_array)
        object.__setattr__(self, "lower", lower_array)
        object.__setattr__(self, "upper", upper_array)
        object.__setattr__(self, "sense", sense_value)
        object.__setattr__(self, "_minimization_y", minimization_y)
        object.__setattr__(self, "_fingerprint", _fingerprint(tensors[:4], sense_value))
        object.__setattr__(self, "_versions", tuple(tensor._version for tensor in tensors))

    @property
    def n_observations(self) -> int:
        return self.x.shape[0]

    @property
    def dimension(self) -> int:
        return self.x.shape[1]

    @property
    def minimization_y(self) -> torch.Tensor:
        return self._minimization_y

    @property
    def fingerprint(self) -> str:
        return self._fingerprint

    @property
    def device_type(self) -> DeviceType:
        device_type = self.x.device.type
        if device_type == "cpu":
            return "cpu"
        if device_type == "cuda":
            return "cuda"
        return "mps"

    @property
    def device_index(self) -> int | None:
        return self.x.device.index

    @property
    def dtype_name(self) -> FloatingDType:
        return "float32" if self.x.dtype is torch.float32 else "float64"

    def validate_unchanged(self) -> None:
        tensors = (self.x, self.y, self.lower, self.upper, self._minimization_y)
        versions = tuple(tensor._version for tensor in tensors)
        if versions != self._versions:
            raise RuntimeError("TensorLandscapeSample tensors must not be modified in place")

x instance-attribute

x: Tensor

y instance-attribute

y: Tensor

lower instance-attribute

lower: Tensor

upper instance-attribute

upper: Tensor

sense instance-attribute

sense: ObjectiveSense

n_observations property

n_observations: int

dimension property

dimension: int

minimization_y property

minimization_y: Tensor

fingerprint property

fingerprint: str

device_type property

device_type: DeviceType

device_index property

device_index: int | None

dtype_name property

dtype_name: FloatingDType

__init__

__init__(
    x: Tensor,
    y: Tensor,
    lower: TensorValue,
    upper: TensorValue,
    sense: ObjectiveSense
    | ObjectiveSenseName = ObjectiveSense.MINIMIZE,
) -> None
Source code in src/orivex/torch/sample.py
def __init__(
    self,
    x: torch.Tensor,
    y: torch.Tensor,
    lower: TensorValue,
    upper: TensorValue,
    sense: ObjectiveSense | ObjectiveSenseName = ObjectiveSense.MINIMIZE,
) -> None:
    if not isinstance(x, torch.Tensor) or not isinstance(y, torch.Tensor):
        raise TypeError("X and y must be torch tensors")
    if x.dtype not in _SUPPORTED_DTYPES or y.dtype not in _SUPPORTED_DTYPES:
        raise TypeError("X and y must have dtype torch.float32 or torch.float64")
    if x.dtype != y.dtype:
        raise ValueError("X and y must have the same dtype")
    if x.device != y.device:
        raise ValueError("X and y must be on the same device")
    if x.device.type not in _SUPPORTED_DEVICES:
        raise ValueError(f"unsupported device type: {x.device.type!r}")

    x_array = x.clone()
    y_array = y.clone()
    lower_array = _clone_bound(lower, reference=x_array, name="lower")
    upper_array = _clone_bound(upper, reference=x_array, name="upper")
    sense_value = ObjectiveSense(sense)

    if x_array.ndim != 2:
        raise ValueError(f"X must be 2-dimensional, got shape {tuple(x_array.shape)}")
    if y_array.ndim != 1:
        raise ValueError(f"y must be 1-dimensional, got shape {tuple(y_array.shape)}")
    observations, dimension = x_array.shape
    if observations == 0 or dimension == 0:
        raise ValueError("X must contain at least one observation and one variable")
    if y_array.shape != (observations,):
        raise ValueError(f"y must have shape ({observations},), got {tuple(y_array.shape)}")
    if lower_array.shape != (dimension,) or upper_array.shape != (dimension,):
        raise ValueError(f"bounds must both have shape ({dimension},)")
    if not bool(torch.isfinite(x_array).all().item()):
        raise ValueError("X must contain only finite values")
    if not bool(torch.isfinite(y_array).all().item()):
        raise ValueError("y must contain only finite values")
    if not bool(torch.isfinite(lower_array).all().item()):
        raise ValueError("lower must contain only finite values")
    if not bool(torch.isfinite(upper_array).all().item()):
        raise ValueError("upper must contain only finite values")
    if not bool((lower_array < upper_array).all().item()):
        raise ValueError("every lower bound must be strictly smaller than its upper bound")
    inside = (x_array >= lower_array) & (x_array <= upper_array)
    if not bool(inside.all().item()):
        raise ValueError("all observations must lie within the inclusive box bounds")

    minimization_y = y_array if sense_value is ObjectiveSense.MINIMIZE else -y_array
    tensors = (x_array, y_array, lower_array, upper_array, minimization_y)
    object.__setattr__(self, "x", x_array)
    object.__setattr__(self, "y", y_array)
    object.__setattr__(self, "lower", lower_array)
    object.__setattr__(self, "upper", upper_array)
    object.__setattr__(self, "sense", sense_value)
    object.__setattr__(self, "_minimization_y", minimization_y)
    object.__setattr__(self, "_fingerprint", _fingerprint(tensors[:4], sense_value))
    object.__setattr__(self, "_versions", tuple(tensor._version for tensor in tensors))

validate_unchanged

validate_unchanged() -> None
Source code in src/orivex/torch/sample.py
def validate_unchanged(self) -> None:
    tensors = (self.x, self.y, self.lower, self.upper, self._minimization_y)
    versions = tuple(tensor._version for tensor in tensors)
    if versions != self._versions:
        raise RuntimeError("TensorLandscapeSample tensors must not be modified in place")

list_features

list_features() -> tuple[FeatureSpec, ...]

Return the mathematical specifications implemented by this backend.

Source code in src/orivex/torch/api.py
def list_features() -> tuple[FeatureSpec, ...]:
    """Return the mathematical specifications implemented by this backend."""

    return tuple(DEFAULT_ENGINE.registry.get(name) for name in DEFAULT_ENGINE.registry.names())

list_capabilities

list_capabilities(
    *, y_normalization: YNormalization = "minmax"
) -> tuple[FeatureCapability, ...]

Return capabilities including a conservative preprocessing autograd guarantee.

Source code in src/orivex/torch/api.py
def list_capabilities(
    *,
    y_normalization: YNormalization = "minmax",
) -> tuple[FeatureCapability, ...]:
    """Return capabilities including a conservative preprocessing autograd guarantee."""

    normalization_definition(y_normalization)
    capabilities = CAPABILITIES
    if y_normalization == "minmax":
        capabilities = tuple(
            replace(
                item,
                autograd="piecewise",
                notes=(
                    *item.notes,
                    "Min-max preprocessing is piecewise differentiable at extrema.",
                ),
            )
            for item in capabilities
        )
    return tuple(sorted(capabilities, key=lambda capability: capability.feature_name))

UnsupportedFeatureError

Bases: ValueError

The requested feature exists in orivex but not in the Torch backend.

Source code in src/orivex/torch/api.py
class UnsupportedFeatureError(ValueError):
    """The requested feature exists in orivex but not in the Torch backend."""

UnsupportedFeatureDeviceError

Bases: ValueError

The requested Torch feature is unavailable on the sample's device type.

Source code in src/orivex/torch/api.py
class UnsupportedFeatureDeviceError(ValueError):
    """The requested Torch feature is unavailable on the sample's device type."""