Source code for materforge.algorithms.interpolation

# SPDX-FileCopyrightText: 2025 - 2026 Rahil Miten Doshi, Friedrich-Alexander-Universität Erlangen-Nürnberg
# SPDX-FileCopyrightText: 2026 Matthias Markl, Friedrich-Alexander-Universität Erlangen-Nürnberg
# SPDX-License-Identifier: BSD-3-Clause

import logging
import numpy as np
from typing import Tuple

from materforge.parsing.config.yaml_keys import LINEAR_KEY
from materforge.data.constants import ProcessingConstants

logger = logging.getLogger(__name__)


[docs] def interpolate_value(dep_value, dep_array: np.ndarray, prop_array: np.ndarray, lower_bound_type: str, upper_bound_type: str): """Interpolates a property value at dep_value using the provided data arrays. Args: dep_value: Dependency value at which to interpolate. dep_array: Dependency axis data (must be sorted ascending). prop_array: Corresponding property values. lower_bound_type: Behaviour below dep_array[0]: CONSTANT_KEY or LINEAR_KEY. upper_bound_type: Behaviour above dep_array[-1]: CONSTANT_KEY or LINEAR_KEY. Returns: Interpolated value. Raises: ValueError: If dep_value is non-finite, arrays are empty or mismatched, or extrapolation is requested on a degenerate interval. """ if not np.isfinite(dep_value): raise ValueError(f"dep_value must be finite, got {dep_value}") if len(dep_array) == 0 or len(prop_array) == 0: raise ValueError("Input arrays cannot be empty") if len(dep_array) != len(prop_array): raise ValueError(f"Array length mismatch: dep_array({len(dep_array)}) != prop_array({len(prop_array)})") logger.debug("Interpolating at dep_value=%.4g (lower=%s, upper=%s)", dep_value, lower_bound_type, upper_bound_type) logger.debug("Data range: dep=[%.4g, %.4g], prop=[%.3e, %.3e]", dep_array[0], dep_array[-1], np.min(prop_array), np.max(prop_array)) try: if dep_value < dep_array[0] and lower_bound_type == LINEAR_KEY: logger.debug("dep_value below data range - applying lower bound: %s", lower_bound_type) if len(dep_array) < 2: return prop_array[0] denom = dep_array[1] - dep_array[0] if denom == 0: raise ValueError("Cannot extrapolate: first two dependency values are equal") slope = (prop_array[1] - prop_array[0]) / denom return prop_array[0] + slope * (dep_value - dep_array[0]) if dep_value >= dep_array[-1] and upper_bound_type == LINEAR_KEY: logger.debug("dep_value above data range - applying upper bound: %s", upper_bound_type) if len(dep_array) < 2: return prop_array[-1] denom = dep_array[-1] - dep_array[-2] if denom == 0: raise ValueError("Cannot extrapolate: last two dependency values are equal") slope = (prop_array[-1] - prop_array[-2]) / denom return float(prop_array[-1] + slope * (dep_value - dep_array[-1])) return float(np.interp(dep_value, dep_array, prop_array)) except Exception as e: raise ValueError(f"Interpolation failed at dep_value={dep_value}: {str(e)}") from e
[docs] def ensure_ascending_order(dep_array: np.ndarray, *value_arrays: np.ndarray) -> Tuple[np.ndarray, ...]: """Ensures dep_array is in ascending order, flipping all arrays together if needed. Args: dep_array: Dependency axis data. ``*value_arrays``: Any number of paired value arrays to flip alongside dep_array. Returns: Tuple of (dep_array, ``*value_arrays``), all in ascending dep order. Raises: ValueError: If the array is neither strictly ascending nor strictly descending. """ if len(dep_array) < 2: return (dep_array,) + value_arrays try: diffs = np.diff(dep_array) tol = ProcessingConstants.FLOATING_POINT_TOLERANCE n = len(diffs) ascending_count = int(np.sum(diffs > tol)) descending_count = int(np.sum(diffs < -tol)) constant_count = int(np.sum(np.abs(diffs) <= tol)) logger.debug("Array diffs: %d ascending, %d descending, %d constant", ascending_count, descending_count, constant_count) is_ascending = ascending_count == n or (ascending_count > 0 and constant_count == n - ascending_count) is_descending = descending_count == n or (descending_count > 0 and constant_count == n - descending_count) if is_ascending: return (dep_array,) + value_arrays if is_descending: logger.debug("Array is descending - flipping all arrays") return (np.flip(dep_array),) + tuple(np.flip(arr) for arr in value_arrays) summary = (str(dep_array.tolist()) if len(dep_array) <= 20 else f"[{dep_array[0]}, ..., {dep_array[-1]}] (length={len(dep_array)})") raise ValueError(f"Array is neither strictly ascending nor descending: {summary}") except Exception as e: raise ValueError(f"Failed to ensure ascending order: {str(e)}") from e