import sympy as sp
from typing import Union
import logging
logger = logging.getLogger(__name__)
[docs]
class PiecewiseInverter:
"""
Creates inverse functions for linear piecewise functions only.
Simplified version that supports only degree 1 polynomial.
"""
def __init__(self, tolerance: float = 1e-12):
"""Initialize the inverter with numerical tolerance."""
self.tolerance = tolerance
logger.debug("PiecewiseInverter initialized with tolerance: %.2e", tolerance)
[docs]
@staticmethod
def create_inverse(piecewise_func: Union[sp.Piecewise, sp.Expr],
input_symbol: Union[sp.Symbol, sp.Basic],
output_symbol: Union[sp.Symbol, sp.Basic],
tolerance: float = 1e-12) -> sp.Piecewise:
"""
Create the inverse of a linear piecewise function.
This static method provides a convenient interface for creating inverse
functions without requiring explicit instantiation of PiecewiseInverter.
Args:
piecewise_func: The original piecewise function E = f(T)
input_symbol: Original input symbol (e.g., T)
output_symbol: Output symbol for inverse function (e.g., E)
tolerance: Numerical tolerance for inversion stability (default: 1e-12)
Returns:
Inverse piecewise function T = f_inv(E)
Raises:
ValueError: If any piece has degree > 1
Examples:
>>> T = sp.Symbol('T')
>>> E = sp.Symbol('E')
>>> piecewise_function = sp.Piecewise((2*T + 100, T < 500), (3*T - 400, True))
>>> inverse = PiecewiseInverter.create_inverse(piecewise_function, T, E)
"""
logger.info("Creating inverse function: %s = f_inv(%s)", input_symbol, output_symbol)
logger.debug("Using tolerance: %.2e", tolerance)
if not isinstance(piecewise_func, sp.Piecewise):
logger.error("Input is not a piecewise function: %s", type(piecewise_func))
raise ValueError(f"Expected Piecewise function, got {type(piecewise_func)}")
inverter = PiecewiseInverter(tolerance)
try:
result = inverter._create_inverse_impl(piecewise_func, input_symbol, output_symbol)
logger.info("Successfully created inverse function")
return result
except Exception as e:
logger.error("Failed to create inverse function: %s", e, exc_info=True)
raise
def _create_inverse_impl(self, piecewise_func: Union[sp.Piecewise, sp.Expr],
input_symbol: Union[sp.Symbol, sp.Basic],
output_symbol: Union[sp.Symbol, sp.Basic]) -> sp.Piecewise:
"""
Internal implementation for creating inverse functions.
This method contains the core logic for inverting piecewise functions,
separated from the public static interface for better maintainability.
Args:
piecewise_func: The original piecewise function
input_symbol: Original input symbol
output_symbol: Output symbol for inverse function
Returns:
Inverse piecewise function
"""
num_pieces = len(piecewise_func.args)
logger.info("Creating inverse for piecewise function with %d pieces", num_pieces)
# Validate that all pieces are linear
logger.debug("Validating linearity of all pieces")
self._validate_linear_only(piecewise_func, input_symbol)
# Process each piece
inverse_conditions = []
for i, (expr, condition) in enumerate(piecewise_func.args):
logger.debug("Processing piece %d: expr=%s, condition=%s", i + 1, expr, condition)
if condition == True: # Final piece
inverse_expr = self._invert_linear_expression(expr, input_symbol, output_symbol)
inverse_conditions.append((inverse_expr, True))
logger.debug("Added final piece with universal condition")
else:
# Extract boundary and create inverse
try:
boundary = self._extract_boundary(condition, input_symbol)
logger.debug("Extracted boundary: %.3f", boundary)
inverse_expr = self._invert_linear_expression(expr, input_symbol, output_symbol)
# Calculate energy at boundary for domain condition
boundary_energy = float(expr.subs(input_symbol, boundary))
inverse_conditions.append((inverse_expr, output_symbol < boundary_energy))
logger.debug("Added piece %d: boundary_energy=%.3f", i + 1, boundary_energy)
except Exception as e:
logger.error("Error processing piece %d: %s", i + 1, e)
raise ValueError(f"Error processing piece {i + 1}: {str(e)}") from e
result = sp.Piecewise(*inverse_conditions)
logger.info("Created inverse function with %d conditions", len(inverse_conditions))
return result
@staticmethod
def _validate_linear_only(piecewise_func: sp.Piecewise, input_symbol: sp.Symbol) -> None:
"""Validate that all pieces are linear (degree <= 1)."""
logger.debug("Validating linearity for %d pieces", len(piecewise_func.args))
for i, (expr, condition) in enumerate(piecewise_func.args):
try:
degree = sp.degree(expr, input_symbol)
logger.debug("Piece %d degree: %d", i + 1, degree)
if degree > 1:
logger.error("Non-linear piece found: piece %d has degree %d", i + 1, degree)
raise ValueError(
f"Piece {i + 1} has degree {degree}. Only linear functions (degree ≤ 1) are supported.")
except Exception as e:
logger.error("Error checking degree for piece %d: %s", i + 1, e)
raise ValueError(f"Error validating piece {i + 1}: {str(e)}") from e
logger.debug("All pieces validated as linear")
@staticmethod
def _extract_boundary(condition: sp.Basic, symbol: sp.Symbol) -> float:
"""Extract the boundary value from a condition like 'T < 300.0'."""
logger.debug("Extracting boundary from condition: %s", condition)
try:
if hasattr(condition, 'rhs'):
boundary = float(condition.rhs)
logger.debug("Extracted boundary from rhs: %.3f", boundary)
return boundary
elif hasattr(condition, 'args') and len(condition.args) == 2:
if condition.args[0] == symbol:
boundary = float(condition.args[1])
logger.debug("Extracted boundary from args[1]: %.3f", boundary)
return boundary
elif condition.args[1] == symbol:
boundary = float(condition.args[0])
logger.debug("Extracted boundary from args[0]: %.3f", boundary)
return boundary
logger.error("Cannot extract boundary from condition: %s", condition)
raise ValueError(f"Cannot extract boundary from condition: {condition}")
except (ValueError, TypeError) as e:
logger.error("Error extracting boundary value: %s", e)
raise ValueError(f"Error extracting boundary from {condition}: {str(e)}") from e
def _invert_linear_expression(self, expr: sp.Expr, input_symbol: sp.Symbol,
output_symbol: sp.Symbol) -> Union[float, sp.Expr]:
"""
Invert a linear expression: ax + b = y → x = (y - b) / a
Args:
expr: Linear expression to invert
input_symbol: Input variable (x)
output_symbol: Output variable (y)
Returns:
Inverted expression
"""
logger.debug("Inverting expression: %s", expr)
try:
degree = sp.degree(expr, input_symbol)
logger.debug("Expression degree: %d", degree)
if degree == 0:
# Constant function - return the constant as temperature
const_value = float(expr)
logger.debug("Constant expression: returning %.3f", const_value)
return const_value
elif degree == 1:
# Linear function: ax + b = y → x = (y - b) / a
coeffs = sp.Poly(expr, input_symbol).all_coeffs()
a, b = float(coeffs[0]), float(coeffs[1])
logger.debug("Linear coefficients: a=%.6f, b=%.6f", a, b)
if abs(a) < self.tolerance:
logger.error("Linear coefficient too small for inversion: %.2e < %.2e", abs(a), self.tolerance)
raise ValueError("Linear coefficient is too small for stable inversion")
result = (output_symbol - b) / a
logger.debug("Inverted linear expression: (%s - %.6f) / %.6f", output_symbol, b, a)
return result
else:
logger.error("Unsupported expression degree: %d", degree)
raise ValueError(f"Expression has degree {degree}, only linear expressions are supported")
except Exception as e:
logger.error("Error inverting expression '%s': %s", expr, e, exc_info=True)
raise ValueError(f"Failed to invert expression {expr}: {str(e)}") from e
[docs]
@staticmethod
def create_energy_density_inverse(material, output_symbol_name: str = 'E') -> sp.Piecewise:
"""
Create inverse function for energy density: T = f_inv(E)
Args:
material: Material object with energy_density property
output_symbol_name: Symbol name for energy density (default: 'E')
Returns:
Inverse piecewise function
Raises:
ValueError: If energy density is not linear piecewise
"""
logger.info("Creating energy density inverse for material: %s", getattr(material, 'name', 'Unknown'))
logger.debug("Output symbol name: %s", output_symbol_name)
if not hasattr(material, 'energy_density'):
logger.error("Material missing energy_density property")
raise ValueError("Material does not have energy_density property")
energy_density_func = material.energy_density
logger.debug("Energy density function type: %s", type(energy_density_func))
if not isinstance(energy_density_func, sp.Piecewise):
logger.error("Energy density is not piecewise: %s", type(energy_density_func))
raise ValueError(f"Energy density must be a piecewise function, found: {type(energy_density_func)}")
# Extract the temperature symbol
symbols = energy_density_func.free_symbols
logger.debug("Found %d free symbols: %s", len(symbols), symbols)
if len(symbols) != 1:
logger.error("Energy density function has %d symbols, expected 1: %s", len(symbols), symbols)
raise ValueError(f"Energy density function must have exactly one symbol, found: {symbols}")
temp_symbol = list(symbols)[0]
logger.info("Using temperature symbol: %s (type: %s)", temp_symbol, type(temp_symbol))
energy_symbol = sp.Symbol(output_symbol_name)
logger.debug("Created energy symbol: %s", energy_symbol)
# Create the inverter and generate inverse
try:
inverter = PiecewiseInverter()
inverse_func = inverter.create_inverse(energy_density_func, temp_symbol, energy_symbol)
logger.info("Successfully created inverse function: %s = f_inv(%s)", temp_symbol, output_symbol_name)
return inverse_func
except Exception as e:
logger.error("Failed to create energy density inverse: %s", e, exc_info=True)
raise ValueError(f"Failed to create energy density inverse: {str(e)}") from e