diff --git a/src/pymatlib/core/typedefs.py b/src/pymatlib/core/typedefs.py index 7201a978cf698bd014e1bff742727e29fcc336cc..f6b69cd4a2cbe01d72c12489f43d0a891f4dfabd 100644 --- a/src/pymatlib/core/typedefs.py +++ b/src/pymatlib/core/typedefs.py @@ -62,6 +62,18 @@ class MaterialProperty: expr: sp.Expr assignments: List[Assignment] = field(default_factory=list) + def __post_init__(self): + """Validate assignments after initialization.""" + if any(assignment is None for assignment in self.assignments): + raise ValueError("None assignments are not allowed") + + # Validate each assignment + for assignment in self.assignments: + if not isinstance(assignment, Assignment): + raise ValueError(f"Invalid assignment type: {type(assignment)}") + if assignment.lhs is None or assignment.rhs is None or assignment.lhs_type is None: + raise ValueError("Assignment fields cannot be None") + def evalf(self, symbol: sp.Symbol, temperature: Union[float, ArrayTypes]) -> Union[float, np.ndarray]: """ Evaluates the material property at specific temperature values. @@ -72,18 +84,44 @@ class MaterialProperty: Returns: Union[float, np.ndarray]: The evaluated property value(s) at the given temperature(s). + + Raises: + TypeError: If: + - symbol is not found in expression or assignments + - temperature contains non-numeric values + - invalid type for temperature """ + # Get all symbols from expression and assignments + expr_symbols = self.expr.free_symbols + assignment_symbols = set().union(*( + assignment.rhs.free_symbols + for assignment in self.assignments + if isinstance(assignment.rhs, sp.Expr) + )) + all_symbols = expr_symbols.union(assignment_symbols) + + # If we have symbols but the provided one isn't among them, raise TypeError + if all_symbols and symbol not in all_symbols: + raise TypeError(f"Symbol {symbol} not found in expression or assignments") + # If the expression has no symbolic variables, return it as a constant float if not self.expr.free_symbols: return float(self.expr) + # Handle array inputs # If temperature is a numpy array, list, or tuple (ArrayTypes), evaluate the property for each temperature if isinstance(temperature, get_args(ArrayTypes)): return np.array([self.evalf(symbol, t) for t in temperature]) # Convert any numpy scalar to Python float - elif isinstance(temperature, np.generic): + if isinstance(temperature, np.floating): + temperature = float(temperature) + + # Convert numeric types to float + try: temperature = float(temperature) + except (TypeError, ValueError): + raise TypeError(f"Temperature must be numeric, got {type(temperature)}") # Prepare substitutions for symbolic assignments substitutions = [(symbol, temperature)] @@ -98,7 +136,6 @@ class MaterialProperty: # Evaluate the material property with the substitutions result = sp.N(self.expr.subs(substitutions)) - # return float(result) # Try to convert the result to float if possible try: return float(result) diff --git a/tests/test_typedefs.py b/tests/test_typedefs.py new file mode 100644 index 0000000000000000000000000000000000000000..dfbbb318f312995c0bb4a414286915a4f6073729 --- /dev/null +++ b/tests/test_typedefs.py @@ -0,0 +1,108 @@ +import pytest +import numpy as np +import sympy as sp +from pymatlib.core.typedefs import Assignment, MaterialProperty, ArrayTypes, PropertyTypes + +def test_assignment(): + """Test Assignment dataclass functionality.""" + # Test basic assignment + x = sp.Symbol('x') + assignment = Assignment(x, 100, 'double') + assert assignment.lhs == x + assert assignment.rhs == 100 + assert assignment.lhs_type == 'double' + + # Test with tuple + v = sp.IndexedBase('v') + assignment = Assignment(v, (1, 2, 3), 'double[]') + assert assignment.lhs == v + assert assignment.rhs == (1, 2, 3) + assert assignment.lhs_type == 'double[]' + +def test_material_property_constant(): + """Test MaterialProperty with constant values.""" + # Test constant property + mp = MaterialProperty(sp.Float(405.)) + assert mp.evalf(sp.Symbol('T'), 100.) == 405.0 + + # Test with assignments + mp.assignments.append(Assignment(sp.Symbol('A'), (100, 200), 'int')) + assert isinstance(mp.assignments, list) + assert len(mp.assignments) == 1 + +def test_material_property_temperature_dependent(): + """Test MaterialProperty with temperature-dependent expressions.""" + T = sp.Symbol('T') + + # Test linear dependency + mp = MaterialProperty(T * 100.) + assert mp.evalf(T, 2.0) == 200.0 + + # Test polynomial + mp = MaterialProperty(T**2 + T + 1) + assert mp.evalf(T, 2.0) == 7.0 + +def test_material_property_indexed(): + """Test MaterialProperty with indexed base expressions.""" + T = sp.Symbol('T') + v = sp.IndexedBase('v') + i = sp.Symbol('i', integer=True) + + mp = MaterialProperty(v[i]) + mp.assignments.extend([ + Assignment(v, (3, 6, 9), 'float'), + Assignment(i, T / 100, 'int') + ]) + + assert mp.evalf(T, 97) == 3 # i=0 + assert mp.evalf(T, 150) == 6 # i=1 + +def test_material_property_errors(): + """Test error handling in MaterialProperty.""" + T = sp.Symbol('T') + X = sp.Symbol('X') # Different symbol + + # Test evaluation with wrong symbol + mp = MaterialProperty(X * 100) + with pytest.raises(TypeError, match="Symbol T not found in expression or assignments"): + mp.evalf(T, 100.0) + + # Test invalid assignments + with pytest.raises(ValueError, match="None assignments are not allowed"): + MaterialProperty(T * 100, assignments=[None]) + +def test_material_property_array_evaluation(): + """Test MaterialProperty evaluation with arrays.""" + T = sp.Symbol('T') + mp = MaterialProperty(T * 100) + + # Test with numpy array + temps = np.array([1.0, 2.0, 3.0]) + result = mp.evalf(T, temps) + assert isinstance(result, np.ndarray) + assert np.allclose(result, temps * 100.0) + + # Test with invalid array values + with pytest.raises(TypeError): + mp.evalf(T, np.array(['a', 'b', 'c'])) + +def test_material_property_piecewise(): + """Test MaterialProperty with piecewise functions.""" + T = sp.Symbol('T') + mp = MaterialProperty(sp.Piecewise( + (100, T < 0), + (200, T >= 100), + (T, True) + )) + + assert mp.evalf(T, -10) == 100 + assert mp.evalf(T, 50) == 50 + assert mp.evalf(T, 150) == 200 + +def test_material_property_numpy_types(): + """Test MaterialProperty with numpy numeric types.""" + T = sp.Symbol('T') + mp = MaterialProperty(T * 100) + + assert mp.evalf(T, np.float32(1.0)) == 100.0 + assert mp.evalf(T, np.float64(1.0)) == 100.0