# 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