diff --git a/apps/tutorials/codegen/HeatEquationKernel.py b/apps/tutorials/codegen/HeatEquationKernel.py index 6348b0d583e1b5a51052dea7d2c9010731104bae..60a9d5a7e93bff7889bfddfaa6b360a7f2764e9e 100644 --- a/apps/tutorials/codegen/HeatEquationKernel.py +++ b/apps/tutorials/codegen/HeatEquationKernel.py @@ -5,7 +5,6 @@ import pystencils as ps from pystencils_walberla import CodeGeneration, generate_sweep from src.materials.alloys import Ti6Al4V from src.materials.alloys.SS316L import SS316L -from src.materials.models import thermal_diffusivity_by_heat_conductivity from src.materials.assignment_converter import assignment_converter with CodeGeneration() as ctx: @@ -25,13 +24,9 @@ with CodeGeneration() as ctx: Ti6Al4V = SS316L.create_SS316L(u.center()) # Convert assignments to pystencils format - subexp, subs = assignment_converter(Ti6Al4V.assignments.items()) + subexp, subs = assignment_converter(Ti6Al4V.thermal_diffusivity.assignments) - if Ti6Al4V.thermal_diffusivity is not None: - subexp.append(ps.Assignment(thermal_diffusivity, Ti6Al4V.thermal_diffusivity)) - else: - thermal_diffusivity_expr = thermal_diffusivity_by_heat_conductivity(Ti6Al4V.heat_conductivity, Ti6Al4V.density, Ti6Al4V.heat_capacity) - subexp.append(ps.Assignment(thermal_diffusivity, thermal_diffusivity_expr)) + subexp.append(ps.Assignment(thermal_diffusivity, Ti6Al4V.thermal_diffusivity.expr)) ac = ps.AssignmentCollection( subexpressions=subexp, @@ -42,6 +37,8 @@ with CodeGeneration() as ctx: ac = ac.new_with_substitutions(subs) + + # ac = ps.simp.simplifications.add_subexpressions_for_divisions(ac) # ac = ps.simp.simplifications.add_subexpressions_for_field_reads(ac) # ac = ps.simp.sympy_cse(ac) diff --git a/src/materials/alloys/Alloy.py b/src/materials/alloys/Alloy.py index cbe3ab2ffbbe62efb7caa6d43ced37da46be9870..23726c14ece0eb4a20bf249637be4b0dff1b19a2 100644 --- a/src/materials/alloys/Alloy.py +++ b/src/materials/alloys/Alloy.py @@ -1,18 +1,39 @@ -from typing import List, Union, Tuple import numpy as np +from typing import List, Tuple, Union from dataclasses import dataclass, field from src.materials.elements import Element, Ti, Al, V from src.materials.elements import interpolate_atomic_number, interpolate_atomic_mass, interpolate_temperature_boil -from src.materials.typedefs import ValueTypes, AssignmentType, PropertyTypes, VariableTypes -from src.materials.interpolators import interpolate_equidistant, interpolate_lookup -from src.materials.data_handler.data_handler import test_equidistant +from src.materials.typedefs import ArrayTypes, PropertyTypes -# Base class for alloys, storing elemental composition and physical properties @dataclass class Alloy: + """ + Represents an alloy composed of various elements with given fractions. + + Attributes: + elements (List[Element]): List of `Element` objects that make up the alloy. + composition (ArrayTypes): List of fractions of each element in the alloy. Fractions should sum to 1.0. + temperature_solidus (float): Solidus temperature of the alloy in Kelvin. + temperature_liquidus (float): Liquidus temperature of the alloy in Kelvin. + atomic_number (float): Average atomic number of the alloy, calculated post-init. + atomic_mass (float): Average atomic mass of the alloy, calculated post-init. + temperature_boil (float): Boiling temperature of the alloy, calculated post-init. + + Optional properties: + density (PropertyTypes): Density of the alloy. + heat_capacity (PropertyTypes): Heat capacity of the alloy. + heat_conductivity (PropertyTypes): Heat conductivity of the alloy. + latent_heat_of_fusion (PropertyTypes): Latent heat of fusion of the alloy. + latent_heat_of_vaporization (PropertyTypes): Latent heat of vaporization of the alloy. + thermal_diffusivity (PropertyTypes): Thermal diffusivity of the alloy. + thermal_expansion_coefficient (PropertyTypes): Thermal expansion coefficient of the alloy. + dynamic_viscosity (PropertyTypes): Dynamic viscosity of the alloy. + kinematic_viscosity (PropertyTypes): Kinematic viscosity of the alloy. + surface_tension (PropertyTypes): Surface tension of the alloy. + """ elements: List[Element] - composition: ValueTypes # List of fractions summing to 1.0 + composition: ArrayTypes # List of fractions summing to 1.0 temperature_solidus: float temperature_liquidus: float @@ -33,65 +54,73 @@ class Alloy: kinematic_viscosity: PropertyTypes = None surface_tension: PropertyTypes = None - # Dictionary for storing calculated assignments - assignments: AssignmentType = field(default_factory=dict, init=False) - def solidification_interval(self) -> Tuple[float, float]: """ - Calculate the solidification interval based on solidus and liquidus temperatures. + Calculate the solidification interval based on solidus and liquidus temperature of the alloy. Returns: - tuple: A tuple containing the solidus and liquidus temperatures. + Tuple[float, float]: A tuple containing the solidus and liquidus temperatures. """ return self.temperature_solidus, self.temperature_liquidus - def __post_init__(self): + def __post_init__(self) -> None: """ - Initialize properties based on elemental composition. - Ensures the composition sums to 1.0, allowing for floating-point precision issues. + Initializes properties based on elemental composition and validates the composition. + + Raises: + ValueError: If the sum of the composition array does not equal 1. """ if not np.isclose(sum(self.composition), 1.0, atol=1e-12): - raise ValueError("The sum of the composition array must be 1, got", sum(self.composition)) + raise ValueError(f"The sum of the composition array must be 1.0, got {sum(self.composition)}") self.atomic_number = interpolate_atomic_number(self.elements, self.composition) self.atomic_mass = interpolate_atomic_mass(self.elements, self.composition) self.temperature_boil = interpolate_temperature_boil(self.elements, self.composition) - def interpolate_property(self, T: VariableTypes, temp_array: ValueTypes, prop_array: ValueTypes, prop_name: str, - temp_array_limit=6, force_lookup=False): - """ - Perform interpolation based on the type of temperature array (equidistant or not). - - :param T: Temperature, either symbolic (sp.Symbol) or numeric (float). - :param temp_array: Array of temperatures. - :param prop_array: Array of property values corresponding to the temperatures. - :param prop_name: Property name for symbolic representation. - :param temp_array_limit: Minimum length of the temperature array to force equidistant interpolation. - :param force_lookup: Boolean to force lookup interpolation regardless of temperature array type. - :return: Interpolated value. - :raises ValueError: If the property name is not a valid attribute. - """ - if not hasattr(self, prop_name): - raise ValueError("Unknown member variable given", prop_name) - incr = test_equidistant(temp_array, 1.0e-3) - if force_lookup or incr is False or len(temp_array) < temp_array_limit: - # print("Performing perform_interpolation -> interpolate_lookup for", prop_name) - result = interpolate_lookup(T, temp_array, prop_array) - else: - # print("Performing perform_interpolation -> interpolate_equidistant for", prop_name) - result, sub_expr = interpolate_equidistant( - T, float(temp_array[0]), float(incr), prop_array, prop_name) - self.assignments.update({prop_name: sub_expr}) - setattr(self, prop_name, result) +if __name__ == '__main__': + try: + # Valid case + Ti64 = Alloy([Ti, Al, V], [0.90, 0.06, 0.04], 1878, 1928) + print(f"Calculated Atomic Number: {0.90 * Ti.atomic_number + 0.06 * Al.atomic_number + 0.04 * V.atomic_number}") + print(f"Alloy Atomic Number: {Ti64.atomic_number}") + print(f"Initial Heat Conductivity: {Ti64.heat_conductivity}") + Ti64.heat_conductivity = 34 + print(f"Updated Heat Conductivity: {Ti64.heat_conductivity}") + print(f"Boiling Temperature (Before Change): {Ti64.temperature_boil}") + Ti64.temperature_boil = 1000 + print(f"Boiling Temperature (After Change): {Ti64.temperature_boil}") + + # Invalid Composition + try: + invalid_alloy = Alloy([Ti, Al], [0.5, 0.5], 1878, 1928) + except ValueError as e: + print(f"Invalid Composition Test Passed: {e}") + # Empty Composition + try: + empty_composition_alloy = Alloy([Ti], [], 1878, 1928) + except ValueError as e: + print(f"Empty Composition Test Passed: {e}") + + # Single Element Alloy + single_element_alloy = Alloy([Ti], [1.0], 1878, 1928) + print(f"Single Element Alloy Atomic Number: {single_element_alloy.atomic_number}") + + # Invalid Property Assignment + try: + Ti64.heat_conductivity = "invalid_value" + except TypeError as e: + print(f"Invalid Property Assignment Test Passed: {e}") + + # Boundary Values for Temperatures + boundary_alloy = Alloy([Ti, Al, V], [0.33, 0.33, 0.34], -273.15, 10000) + print(f"Boundary Temperatures: Solidus={boundary_alloy.temperature_solidus}, Liquidus={boundary_alloy.temperature_liquidus}") + + # Properties Initialization + default_alloy = Alloy([Ti], [1.0], 1878, 1928) + print(f"Default Density: {default_alloy.density}") + + except Exception as e: + print(f"An unexpected error occurred: {e}") -if __name__ == '__main__': - Ti64 = Alloy([Ti, Al, V], [0.90, 0.06, 0.04], 1878, 1928) - print(0.9 * 22 + 0.06 * 13 + 0.04 * 23, Ti64.atomic_number) - print(Ti64.heat_conductivity) - Ti64.heat_conductivity = 34 - print(Ti64.heat_conductivity) - print(Ti64.temperature_boil) - Ti64.temperature_boil = 1000 - print(Ti64.temperature_boil) diff --git a/src/materials/alloys/SS316L/SS316L.py b/src/materials/alloys/SS316L/SS316L.py index 06d3d726dbf7d2a82158d3ee49e76be7b99fec09..df963b13528362e1c72739af4ad5d180e8a592d1 100644 --- a/src/materials/alloys/SS316L/SS316L.py +++ b/src/materials/alloys/SS316L/SS316L.py @@ -1,28 +1,44 @@ - -from pathlib import Path -import numpy as np import sympy as sp +from pathlib import Path +from typing import Union from src.materials.alloys.Alloy import Alloy from src.materials.elements import Fe, Cr, Mn, Ni from src.materials.models import thermal_diffusivity_by_heat_conductivity from src.materials.data_handler.data_handler import read_data, celsius_to_kelvin, thousand_times -from src.materials.typedefs import VariableTypes -from scipy.interpolate import interp1d +from src.materials.interpolators import interpolate_property -def create_SS316L(T: VariableTypes) -> Alloy: +def create_SS316L(T: Union[float, sp.Symbol]) -> Alloy: """ Creates an Alloy instance for SS316L stainless steel with specific properties. + Args: + T (Union[float, sp.Symbol]): Temperature as a symbolic variable or numeric value. + Returns: - Alloy: Initialized SS316L alloy with physical properties. + Tuple[Alloy, Tuple, Tuple, Tuple, Tuple]: Initialized SS316L alloy with physical properties and temperature arrays. + + Notes: + - **Data Units**: All input data should be in SI units. If data is not in SI units, it must be converted. For example: + - **Temperature**: Convert Celsius to Kelvin using `celsius_to_kelvin` function. + - **Density**: Should be in kg/m³. + - **Heat Capacity**: Should be in J/(kg·K). + - **Heat Conductivity**: Should be in W/(m·K). + - **Temperature Array Consistency**: Ensure that all temperature arrays used for interpolation (`density_temp_array`, `heat_capacity_temp_array`, `heat_conductivity_temp_array`) have the same length. + In this implementation, `density_temp_array` is used as the reference array for all properties to ensure consistency. + - **Input Data Files**: The data files (`density_temperature.txt`, `heat_capacity_temperature.txt`, `heat_conductivity_temperature.txt`) must be located in the same directory as this script. + They should contain data in the units specified above. + + Example: + If you have temperature data in Celsius and property data in non-SI units, convert the temperature to Kelvin and property values to SI units before using them. """ # Define the alloy with specific elemental composition and phase transition temperatures SS316L = Alloy( elements=[Fe, Cr, Mn, Ni], composition=[0.708, 0.192, 0.018, 0.082], # Composition: 70.8% Fe, 19.2% Cr, 1.8% Mn, 8.2% Ni temperature_solidus=1395.68, # Solidus temperature in Kelvin - temperature_liquidus=1455.26 # Liquidus temperature in Kelvin + temperature_liquidus=1455.26, # Liquidus temperature in Kelvin + thermal_expansion_coefficient=1.7e-5 # in 1/K ) # Determine the base directory @@ -52,62 +68,18 @@ def create_SS316L(T: VariableTypes) -> Alloy: heat_conductivity_temp_array = celsius_to_kelvin(heat_conductivity_temp_array) - # Set the thermal expansion coefficient for SS316L - SS316L.thermal_expansion_coefficient = 1.7e-5 # Thermal expansion coefficient in 1/K - # Perform interpolation for each property - SS316L.interpolate_property(T, density_temp_array, density_array, 'density') - # SS316L.interpolate_property(T, density_temp_array, density_array, 'density', tolerance=0.01) # Adaptive Interpolation - SS316L.interpolate_property(T, heat_capacity_temp_array, heat_capacity_array, 'heat_capacity') - SS316L.interpolate_property(T, heat_conductivity_temp_array, heat_conductivity_array, 'heat_conductivity') - - # Uncomment - ''' - # Read temperatures for thermal diffusivity calculation - thermal_diffusivity_temp_file_path = str(base_dir / 'thermal_diffusivity_temperature.txt') - thermal_diffusivity_temp_array = np.asarray(read_temp(thermal_diffusivity_temp_file_path), dtype=float) - ''' - - # Create interpolation functions using scipy - ''' - density_interp = interp1d(density_temp_array, density_array, kind='linear', fill_value='extrapolate') - heat_capacity_interp = interp1d(heat_capacity_temp_array, heat_capacity_array, kind='linear', fill_value='extrapolate') - heat_conductivity_interp = interp1d(heat_conductivity_temp_array, heat_conductivity_array, kind='linear', fill_value='extrapolate') - - # Evaluate numerical values - heat_conductivity_values = heat_conductivity_interp(thermal_diffusivity_temp_array) - density_values = density_interp(thermal_diffusivity_temp_array) - heat_capacity_values = heat_capacity_interp(thermal_diffusivity_temp_array) - ''' - - # Create interpolation functions using numpy - ''' - density_poly = np.polyfit(density_temp_array, density_array, deg=3) # Polynomial of degree 3 - density_func = np.poly1d(density_poly) - - heat_capacity_poly = np.polyfit(heat_capacity_temp_array, heat_capacity_array, deg=3) # Polynomial of degree 3 - heat_capacity_func = np.poly1d(heat_capacity_poly) - - heat_conductivity_poly = np.polyfit(heat_conductivity_temp_array, heat_conductivity_array, deg=3) # Polynomial of degree 3 - heat_conductivity_func = np.poly1d(heat_conductivity_poly) - ''' - - # Evaluate numerical values - ''' - heat_conductivity_values = heat_conductivity_func(thermal_diffusivity_temp_array) - density_values = density_func(thermal_diffusivity_temp_array) - heat_capacity_values = heat_capacity_func(thermal_diffusivity_temp_array) + SS316L.density = interpolate_property(T, density_temp_array, density_array) + SS316L.heat_capacity = interpolate_property(T, heat_capacity_temp_array, heat_capacity_array) + SS316L.heat_conductivity = interpolate_property(T, heat_conductivity_temp_array, heat_conductivity_array) # Calculate thermal diffusivity from thermal conductivity, density, and heat capacity - thermal_diffusivity_values = thermal_diffusivity_by_heat_conductivity(heat_conductivity_values, density_values, heat_capacity_values) - - # Interpolate thermal diffusivity - SS316L.interpolate_property(T, thermal_diffusivity_temp_array, np.asarray(thermal_diffusivity_values), "thermal_diffusivity") - ''' - - # Calculate thermal diffusivity from thermal conductivity, density, and heat capacity - SS316L.thermal_diffusivity = thermal_diffusivity_by_heat_conductivity( - SS316L.heat_conductivity, SS316L.density, SS316L.heat_capacity) + SS316L.thermal_diffusivity = interpolate_property(T, density_temp_array, + thermal_diffusivity_by_heat_conductivity( + SS316L.heat_conductivity.evalf(T, density_temp_array), + SS316L.density.evalf(T, density_temp_array), + SS316L.heat_capacity.evalf(T, density_temp_array) + )) return SS316L @@ -120,4 +92,28 @@ if __name__ == '__main__': for i in range(len(alloy.composition)): print(f"Element {alloy.elements[i]}: {alloy.composition[i]}") - print(alloy.thermal_diffusivity) + print("\nTesting SS316L with symbolic temperature:") + for field in vars(alloy): + print(f"{field} = {alloy.__getattribute__(field)}") + + # Test interpolate_property + print("\nTesting interpolate_property:") + test_temp = 1400.0 # Example temperature value + + # Interpolate density, heat capacity, and heat conductivity + density_result = alloy.density.evalf(Temp, test_temp) + print(f"Interpolated density at T={test_temp} using evalf: {density_result}") + + heat_capacity_result = alloy.heat_capacity.evalf(Temp, test_temp) + print(f"Interpolated heat capacity at T={test_temp} using evalf: {heat_capacity_result}") + + heat_conductivity_result = alloy.heat_conductivity.evalf(Temp, test_temp) + print(f"Interpolated heat conductivity at T={test_temp} using evalf: {heat_conductivity_result}") + + # Test thermal diffusivity calculation + heat_conductivity = 500 # Example value for testing + density = 5000 # Example value for testing + heat_capacity = 600 # Example value for testing + thermal_diffusivity = thermal_diffusivity_by_heat_conductivity( + heat_conductivity, density, heat_capacity) + print(f"Calculated thermal diffusivity: {thermal_diffusivity}") \ No newline at end of file diff --git a/src/materials/alloys/Ti6Al4V.py b/src/materials/alloys/Ti6Al4V.py index c49ed721b72cc8c87a6b4074b4edab27eda3239b..6ba95809333b53cd0da0f361f9de0438678f8218 100644 --- a/src/materials/alloys/Ti6Al4V.py +++ b/src/materials/alloys/Ti6Al4V.py @@ -1,68 +1,73 @@ -import numpy as np import sympy as sp +from typing import Union from src.materials.alloys.Alloy import Alloy from src.materials.constants import Constants from src.materials.elements import Ti, Al, V -from src.materials.interpolators import interpolate_equidistant, interpolate_lookup +from src.materials.interpolators import interpolate_equidistant, interpolate_lookup, interpolate_property from src.materials.models import density_by_thermal_expansion, thermal_diffusivity_by_heat_conductivity -from src.materials.typedefs import VariableTypes +from src.materials.typedefs import MaterialProperty -def create_Ti6Al4V(T: VariableTypes) -> Alloy: +def create_Ti6Al4V(T: Union[float, sp.Symbol]) -> Alloy: """ Creates an Alloy instance for Ti6Al4V with specific properties. Args: - T (VariableTypes): Temperature as a symbolic variable or numeric value. + T (Union[float, sp.Symbol]): Temperature as a symbolic variable or numeric value. Returns: - Alloy: Initialized Ti6Al4V alloy with physical properties. + Alloy: Initialized Ti6Al4V alloy with physical properties including density, heat conductivity, and thermal diffusivity. + + Notes: + - **Data Units**: All input data should be in SI units. If data is not in SI units, it must be converted. For example: + - **Temperature**: The temperature used in the function is expected to be in Kelvin. If you have temperature data in Celsius, convert it to Kelvin before using it. + - **Density**: Should be in kg/m³. + - **Heat Capacity**: Should be in J/(kg·K). + - **Heat Conductivity**: Should be in W/(m·K). + - **Temperature Array Consistency**: Ensure that all temperature arrays used for interpolation (`density_temp_array`, `heat_capacity_temp_array`, `heat_conductivity_temp_array`) have the same length to ensure consistency in property evaluation. + The current implementation uses the temperature range from the `solidification_interval` method for all interpolations to maintain consistency. + - **Solidus and Liquidus**: The solidus and liquidus temperatures define the temperature range over which the alloy is solid and liquid, respectively. These temperatures are used to determine the range for property interpolation. + - **Thermal Diffusivity Calculation**: The thermal diffusivity is computed using the formula: + + Thermal Diffusivity = Heat Conductivity / Density * Heat Capacity + + where: + - **Heat Conductivity**: Interpolated value based on temperature. + - **Density**: Computed based on thermal expansion. + - **Heat Capacity**: Provided directly. + + Example: + If you have temperature data in Celsius and property data in non-SI units, convert the temperature to Kelvin and property values to SI units before using them. + For Ti6Al4V, the alloy properties are computed based on the provided temperature and constants, ensuring accurate representation of the material's behavior. + """ - # Define alloy with specific elemental composition and phase transition temperatures + # Define the Ti6Al4V alloy with its elemental composition and phase transition temperatures Ti6Al4V = Alloy( elements=[Ti, Al, V], - composition=[0.90, 0.06, 0.04], # 90% Ti, 6% Al, 4% V - temperature_solidus=1878.0, # Temperature solidus in Kelvin - temperature_liquidus=1928.0 # Temperature liquidus in Kelvin + composition=[0.90, 0.06, 0.04], # 90% Ti, 6% Al, 4% V + temperature_solidus=1878.0, # Temperature solidus in Kelvin + temperature_liquidus=1928.0, # Temperature liquidus in Kelvin + thermal_expansion_coefficient=8.6e-6, # 1/K [Source: MatWeb] + heat_capacity=526, # J/(kg·K) [Source: MatWeb] + latent_heat_of_fusion=290000.0, # J/kg [Source: MatWeb] + latent_heat_of_vaporization=8.86e6 # J/kg [Source: MatWeb] ) - # Set properties specific to Ti6Al4V - # Thermal expansion coefficient (1/K) [Source: MatWeb] - Ti6Al4V.thermal_expansion_coefficient = 8.6e-6 # 1/K - - # Define density using models and interpolations for different temperature ranges - # Density (kg/m³) [Source: MatWeb] - # Ti6Al4V.density = 4430.0 # kg/m³ - - # Using interpolate_equidistant for density - T_array = np.linspace(Ti6Al4V.temperature_solidus, Ti6Al4V.temperature_liquidus, 7) - # density_array = [4236.3, 4234.9, 4233.6, 4232.3, 4230.9, 4229.7, 4227.0] - density_array = [density_by_thermal_expansion( - T_i, Constants.temperature_room, 4430, Ti6Al4V.thermal_expansion_coefficient) for T_i in T_array] - # Perform equidistant interpolation for the density at temperature T - density_result, sub_expr_density = interpolate_equidistant( - T, Ti6Al4V.temperature_solidus, 8, density_array, 'density') - # Update the alloy with the interpolated density value - Ti6Al4V.density = density_result - # Store the sub-expressions for symbolic evaluation and further processing - Ti6Al4V.assignments.update({"density": sub_expr_density}) - - # Specific heat capacity (J/(kg·K)) [Source: MatWeb] - Ti6Al4V.heat_capacity = 526 # J/(kg·K) - - # Thermal conductivity (W/(m·K)) [Source: MatWeb] - Ti6Al4V.heat_conductivity = interpolate_lookup( - T, Ti6Al4V.solidification_interval(), [6.7, 32.0]) # W/(m·K) - - # Latent heat of fusion (J/kg) [Source: MatWeb] - Ti6Al4V.latent_heat_of_fusion = 290000.0 # J/kg + # Compute density based on thermal expansion and other constants + Ti6Al4V.density = MaterialProperty(density_by_thermal_expansion( + T, Constants.temperature_room, 4430, Ti6Al4V.thermal_expansion_coefficient)) - # Latent heat of vaporization (J/kg) [Source: MatWeb] - Ti6Al4V.latent_heat_of_vaporization = 8.86e6 # J/kg + # Interpolate heat conductivity based on temperature + Ti6Al4V.heat_conductivity = interpolate_property( + T, Ti6Al4V.solidification_interval(), [6.7, 32.0]) # W/(m·K) - # Calculate thermal diffusivity from conductivity, density, and heat capacity - Ti6Al4V.thermal_diffusivity = thermal_diffusivity_by_heat_conductivity( - Ti6Al4V.heat_conductivity, Ti6Al4V.density, Ti6Al4V.heat_capacity) + # Calculate thermal diffusivity using conductivity, density, and heat capacity + Ti6Al4V.thermal_diffusivity = interpolate_property(T, Ti6Al4V.solidification_interval(), + thermal_diffusivity_by_heat_conductivity( + Ti6Al4V.heat_conductivity.evalf(T, Ti6Al4V.solidification_interval()), + Ti6Al4V.density.expr, + Ti6Al4V.heat_capacity + )) return Ti6Al4V @@ -84,16 +89,13 @@ if __name__ == '__main__': # Test interpolate_equidistant print("\nTesting interpolate_equidistant:") test_temp = 1850.0 # Example temperature value - result, _ = interpolate_equidistant( - test_temp, 1820, 20.0, [700, 800, 900, 1000, 1100, 1200], 'heat_capacity') - print(f"Interpolated heat capacity at T={test_temp} using interpolate_equidistant: {result}") - - _, assignments = interpolate_equidistant( - Temp, 1820, 20.0, [700, 800, 900, 1000, 1100, 1200], 'heat_capacity') - print(f"Assignments: {assignments}") + result = interpolate_equidistant( + Temp, 1820, 20.0, [700, 800, 900, 1000, 1100, 1200]) + print(f"Interpolated heat capacity at T={test_temp} using interpolate_equidistant: {result.expr}") + print(f"Assignments: {result.assignments}") - result_density, _ = interpolate_equidistant( - test_temp, 1878, 8, [4236.3, 4234.9, 4233.6, 4232.3, 4230.9, 4229.7, 4227.0], 'density') + result_density = interpolate_equidistant( + test_temp, 1878, 8, [4236.3, 4234.9, 4233.6, 4232.3, 4230.9, 4229.7, 4227.0]) print(f"Interpolated density at T={test_temp} using interpolate_equidistant: {result_density}") # Test interpolate_lookup @@ -111,8 +113,3 @@ if __name__ == '__main__': thermal_diffusivity = thermal_diffusivity_by_heat_conductivity( heat_conductivity, density, heat_capacity) print(f"Calculated thermal diffusivity: {thermal_diffusivity}") - - # Uncomment below lines if plotting is needed - # sp.plot(alloy.heat_conductivity, (Temp, 1800, 2000)) - # sp.plot(alloy.density, (Temp, 1800, 2000)) - # sp.plot(alloy.heat_capacity.subs([(alloy.assignments['heat_capacity'][1][0], alloy.assignments['heat_capacity'][1][1]), (alloy.assignments['heat_capacity'][0][0], alloy.assignments['heat_capacity'][0][1])]), (Temp, 1800, 2000)) diff --git a/src/materials/assignment_converter.py b/src/materials/assignment_converter.py index f8f0a304f5ee7db4ae5975e29335f210edb5af3b..043634c55363e556ef89c06feb88073a97b52bd1 100644 --- a/src/materials/assignment_converter.py +++ b/src/materials/assignment_converter.py @@ -1,10 +1,13 @@ import numpy as np +import sympy as sp import pystencils as ps -from pystencils.types.quick import * +from typing import List, Dict, Tuple, Union +from pystencils.types.quick import Arr, Fp from pystencils.sympyextensions.typed_sympy import CastFunc +from src.materials.typedefs import Assignment -def type_mapping(type_str: str): +def type_mapping(type_str: str) -> Union[np.dtype, Arr]: """ Maps a string representation of a type to a corresponding numpy or pystencils data type. @@ -25,36 +28,71 @@ def type_mapping(type_str: str): return np.dtype(type_str) # Default to numpy data type -def assignment_converter(assignments_items): +def assignment_converter(assignment_list: List[Assignment]) \ + -> Tuple[List[ps.Assignment], Dict[sp.Symbol, ps.TypedSymbol]]: """ - Converts the assignment data from the Alloy class to pystencils-compatible format. + Converts a list of assignments from the Alloy class to pystencils-compatible format. Parameters: - assignments_items (Dict[str, List[Assignment]]): A dictionary where each key is a label (string) and each value is a list of `Assignment` objects. - Each `Assignment` object should have the following attributes: + assignment_list (List[sp.Assignment]): A list of `Assignment` objects where each `Assignment` object should have: - lhs (sp.Symbol): The left-hand side of the assignment, which is a symbolic variable. It should have a `name` and a `type` attribute. - rhs (Union[tuple, sp.Expr]): The right-hand side of the assignment, which can be a tuple or a sympy expression. - lhs_type (str): A string representing the type of the left-hand side, indicating the data type (e.g., "double[]", "float[]"). Returns: - Tuple[List[ps.Assignment], Dict[Assignment, ps.TypedSymbol]]: + Tuple[List[ps.Assignment], Dict[sp.Symbol, ps.TypedSymbol]]: - A list of `pystencils.Assignment` objects, each representing an assignment in the pystencils format. - This list includes assignments where the `rhs` is directly assigned or cast to the appropriate type. - - A dictionary mapping each original `Assignment` object to its corresponding `pystencils.TypedSymbol`. - The dictionary maps `sp.Symbol` objects to `pystencils.TypedSymbol` instances, allowing for easy retrieval of types and assignments. + This list includes assignments where the `rhs` is directly assigned or cast to the appropriate type. + - A dictionary mapping each original `sp.Symbol` object (from the lhs) to its corresponding `pystencils.TypedSymbol`. + The dictionary allows for easy retrieval of types and assignments. """ subexp = [] subs = {} - # Iterate over the dictionary items - for label, assignment_list in assignments_items: - print('assignment_list: ', assignment_list) - for assignment in assignment_list: - print('assignment in assignment_list: ', assignment) - ps_type = type_mapping(assignment.lhs_type) - data_symbol = ps.TypedSymbol(assignment.lhs.name, ps_type) - if isinstance(ps_type, Arr): - subexp.append(ps.Assignment(data_symbol, assignment.rhs)) - else: # needed for the sp.floor in interpolate_equidistant to cast it to an int from double - subexp.append(ps.Assignment(data_symbol, CastFunc(assignment.rhs, ps_type))) - subs[assignment.lhs] = data_symbol + + for assignment in assignment_list: + ps_type = type_mapping(assignment.lhs_type) + data_symbol = ps.TypedSymbol(assignment.lhs.name, ps_type) + + if isinstance(ps_type, Arr): + subexp.append(ps.Assignment(data_symbol, assignment.rhs)) + else: # Cast rhs to the appropriate type when necessary + subexp.append(ps.Assignment(data_symbol, CastFunc(assignment.rhs, ps_type))) + + subs[assignment.lhs] = data_symbol + return subexp, subs + + +if __name__ == '__main__': + # Define test symbols and assignments + x = sp.Symbol('x') + y = sp.Symbol('y') + z = sp.Symbol('z') + + # Example assignments + assignments = [ + Assignment(lhs=x, rhs=sp.Symbol('value_x'), lhs_type='double[]'), + Assignment(lhs=y, rhs=sp.Symbol('value_y'), lhs_type='float[]'), + Assignment(lhs=z, rhs=sp.Symbol('value_z'), lhs_type='int') + ] + + # Test type_mapping function + print("Testing type_mapping function:") + print(type_mapping("double[]")) # Expected: Arr(Fp(64)) + print(type_mapping("float[]")) # Expected: Arr(Fp(32)) + print(type_mapping("int")) # Expected: np.int32 or equivalent numpy type + + # Test assignment_converter function + print("\nTesting assignment_converter function:") + assignments_converted, symbol_map = assignment_converter(assignments) + + for conv in assignments_converted: + print(conv) + + print("\nSymbol to TypedSymbol mapping:") + for sym, typed_sym in symbol_map.items(): + print(f"{sym}: {typed_sym}") + + # Validate specific conditions for tests + assert isinstance(assignments_converted[0], ps.Assignment), "The converted assignment should be of type pystencils.Assignment" + assert isinstance(symbol_map[x], ps.TypedSymbol), "The symbol map should map symbols to pystencils.TypedSymbol" diff --git a/src/materials/data_handler/data_handler.py b/src/materials/data_handler/data_handler.py index b582b3c043100df0e65bb84c6adbf2aae6e364bc..1b4844f3b42e846a30c201a2e71ce47a4306d8e0 100644 --- a/src/materials/data_handler/data_handler.py +++ b/src/materials/data_handler/data_handler.py @@ -1,13 +1,11 @@ -import os import numpy as np from typing import Union, Tuple -from scipy.interpolate import interp1d, CubicSpline, InterpolatedUnivariateSpline, Rbf from src.materials.data_handler.create_test_files import create_test_files def print_results(file_path: str, temperatures: np.ndarray, material_property: np.ndarray) -> None: """ - Prints the results. + Prints the results of the test. Parameters: file_path (str): The path to the data file. @@ -20,107 +18,6 @@ def print_results(file_path: str, temperatures: np.ndarray, material_property: n print("-" * 40) -def interpolate_missing_values(temp: np.ndarray, prop: np.ndarray, target_temps: np.ndarray) \ - -> Tuple[np.ndarray, np.ndarray]: - """ - Interpolates property values based on existing temperature data and target temperatures. - - Parameters: - temp (np.ndarray): Array of existing temperature values. - prop (np.ndarray): Array of property values corresponding to the temperatures. - target_temps (np.ndarray): Array of target temperatures for interpolation. - - Returns: - Tuple[np.ndarray, np.ndarray]: Target temperatures and interpolated property values. - """ - # Perform linear interpolation using numpy.interp - # interpolated_props = np.interp(target_temps, temp, prop, left=np.nan, right=np.nan) - # return target_temps, interpolated_props - - # Perform linear interpolation using scipy.interpolate.interp1d - interp_func = interp1d(temp, prop, kind='quadratic', fill_value='extrapolate') - # interp_func = CubicSpline(temp, prop) - # interp_func = InterpolatedUnivariateSpline(temp, prop) - interpolated_props = interp_func(target_temps) - return target_temps, interpolated_props - # rbf = Rbf(temp, prop, function='multiquadric') - # return target_temps, rbf(target_temps) - - -def clean_temperature_data(temp: np.ndarray, prop: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: - """ - Cleans the temperature data by ensuring equally spaced values based on the most common temperature difference. - Interpolates missing temperatures if necessary. - - Parameters: - temp (np.ndarray): Array of temperature values. - prop (np.ndarray): Array of property values corresponding to the temperatures. - - Returns: - Tuple[np.ndarray, np.ndarray]: Cleaned temperatures and properties. - """ - if len(temp) < 2: - return temp, prop - - min_temp = np.min(temp) - max_temp = np.max(temp) - - # Check and flip data if in descending order - if np.all(np.diff(temp) < 0): - temp = np.flip(temp) - prop = np.flip(prop) - print("Data was in descending order. Flipped to ascending.") - - diffs = np.diff(temp) - print(f"Differences between consecutive temperatures: {diffs}") - - # Calculate the most common difference - try: - # common_diff = np.median(diffs) # Assuming the most common diff is the desired spacing - # hist, bin_edges = np.histogram(diffs, bins='auto') - # common_diff = bin_edges[np.argmax(hist)] # Most frequent temperature difference - unique_diffs, counts = np.unique(diffs, return_counts=True) - common_diff = unique_diffs[np.argmax(counts)] - if np.max(counts) < 2: - print("No repeated temperature differences found. Returning original data.") - return temp, prop - print(f"Most common temperature difference: {common_diff}") - except Exception as e: - print(f"Error calculating most common difference: {e}") - return temp, prop - - if common_diff <= 0: - raise ValueError("Temperature difference must be positive.") - - # start_temp = min_temp - (min_temp % common_diff) # Add boundary points by extrapolating out of bounds if required - # expected_temps = np.arange(start_temp, max_temp + common_diff, common_diff) # Add boundary points by extrapolating out of bounds if required - - start_temp = min_temp + common_diff - (min_temp % common_diff) if min_temp % common_diff != 0 else min_temp # Cut boundary points if not equidistant - end_temp = max_temp - (max_temp % common_diff) if max_temp % common_diff != 0 else max_temp # Cut boundary points if not equidistant - expected_temps = np.arange(start_temp, end_temp + common_diff, common_diff) - - expected_temps = np.unique(expected_temps) # Remove duplicates - print(f"Expected temperatures: {expected_temps}") - - # Handle case where expected_temps is empty - if len(expected_temps) == 0: - print("No expected temperatures generated, falling back to original temperatures.") - return temp, prop - - # Interpolate properties for the expected temperatures - cleaned_temp, cleaned_prop = interpolate_missing_values(temp, prop, expected_temps) - - print(f"Cleaned temperatures: {cleaned_temp}") - print(f"Cleaned properties: {cleaned_prop}") - - # Ensure the final output is in ascending order - sorted_indices = np.argsort(cleaned_temp) - cleaned_temp = cleaned_temp[sorted_indices] - cleaned_prop = cleaned_prop[sorted_indices] - - return cleaned_temp, cleaned_prop - - def read_data(file_path: str, header: bool = True) -> Tuple[np.ndarray, np.ndarray]: """ Reads temperature and property data from a file and returns both the original and cleaned data. @@ -161,16 +58,7 @@ def read_data(file_path: str, header: bool = True) -> Tuple[np.ndarray, np.ndarr duplicate_rows = [str(idx + 1) for idx, value in enumerate(temp) if value in duplicates] raise ValueError(f"Duplicate temperature entries found in rows: {', '.join(duplicate_rows)}") - print(f"Original temperatures: {temp}") - print(f"Original properties: {prop}") - - # Check if the data is already equidistant - if np.all(np.diff(temp) == np.diff(temp)[0]): - print("Data is already equidistant. No cleaning needed.") - return temp, prop - - cleaned_temp, cleaned_prop = clean_temperature_data(temp, prop) - return cleaned_temp, cleaned_prop + return temp, prop except Exception as e: print(f"Error reading data from {file_path}: {e}") @@ -253,19 +141,21 @@ def run_tests() -> None: """ test_cases = [ {"name": "Normal Case", "file_path": "test_files/test_data_normal.txt"}, - # {"name": "Normal Case 1", "file_path": "test_files/test_data_normal1.txt"}, - # {"name": "Case with Variable Spacing", "file_path": "test_files/test_data_variable_spacing.txt"}, - # {"name": "Case with Sparse Data", "file_path": "test_files/test_data_sparse.txt"}, - # {"name": "Case with Missing Values", "file_path": "test_files/test_data_missing_values.txt"}, - # {"name": "Case with Descending Data", "file_path": "test_files/test_data_descending.txt"}, - # {"name": "Case with Duplicates", "file_path": "test_files/test_data_duplicates.txt"}, - # {"name": "Empty File", "file_path": "test_files/test_data_empty.txt"}, - # {"name": "File with Only Header", "file_path": "test_files/test_data_header.txt"}, - # {"name": "Invalid Data Entries", "file_path": "test_files/test_data_invalid_data.txt"}, - # {"name": "Mismatched Columns", "file_path": "test_files/test_data_mismatched_columns.txt"}, - # {"name": "Missing Data Entries", "file_path": "test_files/test_data_missing_data.txt"}, - # {"name": "Ascending density_temperature values", "file_path": "test_files/test_data_density_temperature_ascending.txt"}, - # {"name": "Descending density_temperature values", "file_path": "test_files/test_data_density_temperature_descending.txt"} + {"name": "Normal Case 1", "file_path": "test_files/test_data_normal1.txt"}, + {"name": "Case with Variable Spacing", "file_path": "test_files/test_data_variable_spacing.txt"}, + {"name": "Case with Sparse Data", "file_path": "test_files/test_data_sparse.txt"}, + {"name": "Case with Missing Values", "file_path": "test_files/test_data_missing_values.txt"}, + {"name": "Case with Descending Data", "file_path": "test_files/test_data_descending.txt"}, + {"name": "Case with Duplicates", "file_path": "test_files/test_data_duplicates.txt"}, + {"name": "Empty File", "file_path": "test_files/test_data_empty.txt"}, + {"name": "File with Only Header", "file_path": "test_files/test_data_header.txt"}, + {"name": "Invalid Data Entries", "file_path": "test_files/test_data_invalid_data.txt"}, + {"name": "Mismatched Columns", "file_path": "test_files/test_data_mismatched_columns.txt"}, + {"name": "Missing Data Entries", "file_path": "test_files/test_data_missing_data.txt"}, + {"name": "Ascending density_temperature values", + "file_path": "test_files/test_data_density_temperature_ascending.txt"}, + {"name": "Descending density_temperature values", + "file_path": "test_files/test_data_density_temperature_descending.txt"} ] for case in test_cases: @@ -296,5 +186,5 @@ def run_tests() -> None: if __name__ == "__main__": - create_test_files() + create_test_files() # Ensure test files are created before running tests run_tests() diff --git a/src/materials/interpolators.py b/src/materials/interpolators.py index a308b8fb9ca8e34e77f03b75e895773f39cec04e..1a8b7341e26727a02e8a81eab79915f5b17e52f2 100644 --- a/src/materials/interpolators.py +++ b/src/materials/interpolators.py @@ -1,15 +1,16 @@ import numpy as np import sympy as sp -from typing import List, Tuple, Dict -from src.materials.typedefs import * -# from src.materials.data_handler.data_handler import test_equidistant +from typing import Union +from src.materials.typedefs import Assignment, ArrayTypes, MaterialProperty +COUNT = 0 -def interpolate_equidistant(T: VariableTypes, - T_base: float, - T_incr: float, - v_array: ValueTypes, - label: str) -> Tuple[PropertyTypes, List[Assignment]]: + +def interpolate_equidistant( + T: Union[float, sp.Symbol], + T_base: float, + T_incr: float, + v_array: ArrayTypes) -> Union[float, MaterialProperty]: """ Perform equidistant interpolation for symbolic or numeric temperature values. @@ -17,73 +18,75 @@ def interpolate_equidistant(T: VariableTypes, :param T_base: Base temperature for the interpolation. :param T_incr: Increment in temperature for each step in the interpolation array. :param v_array: Array of values corresponding to the temperatures. - :param label: Label for the symbolic representation. - :return: Interpolated value and list of assignments (if symbolic). + :return: Interpolated value as a float or MaterialProperty object. :raises ValueError: If the temperature type is unsupported. """ if T_incr < 0: T_incr *= -1 - T_base = T_base - T_incr * (len(v_array) - 1) + T_base -= T_incr * (len(v_array) - 1) v_array = np.flip(v_array) if isinstance(T, sp.Symbol): - # print("Performing interpolate_equidistant (symbolic)") - sym_data = sp.IndexedBase(label, shape=len(v_array)) + global COUNT + label = '_' + str(COUNT).zfill(3) + COUNT += 1 + + sym_data = sp.IndexedBase(label + "_var", shape=len(v_array)) sym_idx = sp.symbols(label + '_idx', cls=sp.Idx) - sym_dec = sp.Symbol(label + "_pos_dec") + sym_dec = sp.Symbol(label + "_dec") T_base = sp.Float(T_base) T_incr = sp.Float(T_incr) pos = (T - T_base) / T_incr - pos_dec = pos - sp.floor(pos) # sp.frac(pos) not supported by pystencils yet! + pos_dec = pos - sp.floor(pos) result = sp.Piecewise( (sp.Float(float(v_array[0])), T < T_base), (sp.Float(float(v_array[-1])), T >= T_base + (len(v_array) - 1) * T_incr), - ((1 - sym_dec) * sym_data[sym_idx] + sym_dec * sym_data[sym_idx + 1], True)) + ((1 - sym_dec) * sym_data[sym_idx] + sym_dec * sym_data[sym_idx + 1], True) + ) sub_expressions = [ Assignment(sym_data, tuple(sp.Float(float(v)) for v in v_array), "double[]"), Assignment(sym_idx, pos, "int"), Assignment(sym_dec, pos_dec, "double") ] - return result, sub_expressions + return MaterialProperty(result, sub_expressions) elif isinstance(T, float): - # print("Performing interpolate_equidistant (numeric)") v_array = np.asarray(v_array) n = len(v_array) min_temp = T_base max_temp = T_base + (n - 1) * T_incr if T < min_temp: - # print(f"T ({T}) is below minimum temperature ({min_temp}), returning first value: {v_array[0]}.") - return float(v_array[0]), [] + return float(v_array[0]) elif T > max_temp: - # print(f"T ({T}) is above maximum temperature ({max_temp}), returning last value: {v_array[-1]}.") - return float(v_array[-1]), [] - pos = (T - T_base) / T_incr + return float(v_array[-1]) + pos = (T - T_base) / T_incr pos_int = int(pos) pos_dec = pos - pos_int value = (1 - pos_dec) * v_array[pos_int] + pos_dec * v_array[pos_int + 1] - print(f"Interpolated value at T ({T}): {value}") - return float(value), [] + return float(value) else: raise ValueError(f"Unsupported type for T: {type(T)}") -def interpolate_lookup(T: VariableTypes, T_array: ValueTypes, v_array: ValueTypes) -> PropertyTypes: +def interpolate_lookup( + T: Union[float, sp.Symbol], + T_array: ArrayTypes, + v_array: ArrayTypes) -> Union[float, MaterialProperty]: """ Perform lookup-based interpolation for symbolic or numeric temperature values. :param T: Temperature, either symbolic (sp.Symbol) or numeric (float). :param T_array: Array of temperatures for lookup. :param v_array: Array of values corresponding to the temperatures. - :return: Interpolated value. + :return: Interpolated value as a float or MaterialProperty object. :raises ValueError: If T_array and v_array lengths do not match or if T is of unsupported type. """ if len(T_array) != len(v_array): @@ -94,9 +97,8 @@ def interpolate_lookup(T: VariableTypes, T_array: ValueTypes, v_array: ValueType v_array = np.flip(v_array) if isinstance(T, sp.Symbol): - # print("Performing interpolate_lookup (symbolic)") T_array = [sp.Float(float(t)) for t in T_array] - v_array = [sp.Float(float(v)) for v in v_array] + v_array = [v if isinstance(v, sp.Expr) else sp.Float(float(v)) for v in v_array] conditions = [ (v_array[0], T < T_array[0]), @@ -106,10 +108,9 @@ def interpolate_lookup(T: VariableTypes, T_array: ValueTypes, v_array: ValueType v_array[i] + (v_array[i + 1] - v_array[i]) / (T_array[i + 1] - T_array[i]) * (T - T_array[i])) conditions.append( (interp_expr, sp.And(T >= T_array[i], T < T_array[i + 1]) if i + 1 < len(T_array) - 1 else True)) - return sp.Piecewise(*conditions) + return MaterialProperty(sp.Piecewise(*conditions)) elif isinstance(T, float): - # print("Performing interpolate_lookup (numeric)") T_array = np.asarray(T_array) v_array = np.asarray(v_array) if T < T_array[0]: @@ -119,26 +120,75 @@ def interpolate_lookup(T: VariableTypes, T_array: ValueTypes, v_array: ValueType else: return float(np.interp(T, T_array, v_array)) - raise ValueError(f"Invalid input for T: Union[float, sp.Symbol] got {type(T)}") + else: + raise ValueError(f"Invalid input for T: {type(T)}") + + +def test_equidistant(temp: np.ndarray) -> float: + """ + Tests if the temperature values are equidistant. + + :param temp: Array of temperature values. + :return: The common difference if equidistant, otherwise 0. + """ + temperature_diffs = np.diff(temp) + if np.allclose(temperature_diffs, temperature_diffs[0], atol=1e-12): + return float(temperature_diffs[0]) + return 0 + + +def interpolate_property( + T: Union[float, sp.Symbol], + temp_array: ArrayTypes, + prop_array: ArrayTypes, + temp_array_limit=6, + force_lookup=False) -> Union[float, MaterialProperty]: + """ + Perform interpolation based on the type of temperature array (equidistant or not). + + :param T: Temperature, either symbolic (sp.Symbol) or numeric (float). + :param temp_array: Array of temperatures. + :param prop_array: Array of property values corresponding to the temperatures. + :param temp_array_limit: Minimum length of the temperature array to force equidistant interpolation. + :param force_lookup: Boolean to force lookup interpolation regardless of temperature array type. + :return: Interpolated value. + :raises ValueError: If T_array and v_array lengths do not match. + """ + if len(temp_array) != len(prop_array): + raise ValueError("T_array and v_array must have the same length") + + if temp_array[0] > temp_array[-1]: + temp_array = np.flip(temp_array) + prop_array = np.flip(prop_array) + + incr = test_equidistant(np.asarray(temp_array)) + + if force_lookup or incr == 0 or len(temp_array) < temp_array_limit: + return interpolate_lookup(T, temp_array, prop_array) + else: + return interpolate_equidistant( + T, float(temp_array[0]), float(incr), prop_array) if __name__ == '__main__': # Example usage and consistency tests for the interpolation functions Temp = sp.Symbol('Temp') - density_temp_array1 = np.array([600.0, 500.0, 400.0, 300.0, 200.0]) - density_v_array1 = np.array([10.0, 20.0, 30.0, 40.0, 50.0]) + density_temp_array1 = np.flip(np.array([600.0, 500.0, 400.0, 300.0, 200.0])) + density_v_array1 = np.flip(np.array([10.0, 20.0, 30.0, 40.0, 50.0])) density_temp_array2 = [1878.0, 1884.2, 1894.7, 1905.3, 1915.8, 1926.3, 1928.0] density_v_array2 = [4236.3, 4234.9, 4233.6, 4232.3, 4230.9, 4229.7, 4227.0] - Temp_base = 600.0 - Temp_incr = -100.0 + Temp_base = 200.0 + Temp_incr = 100.0 # Interpolation Lookup Tests print("Interpolate Lookup Tests:") symbolic_result = interpolate_lookup(Temp, density_temp_array1, density_v_array1) - print("Symbolic interpolation result with interpolate_lookup (arrays):", symbolic_result) + print("Symbolic interpolation result with interpolate_lookup (arrays):", + symbolic_result.expr if isinstance(symbolic_result, MaterialProperty) else symbolic_result) symbolic_result_list = interpolate_lookup(Temp, density_temp_array2, density_v_array2) - print("Symbolic interpolation result with interpolate_lookup (lists):", symbolic_result_list) + print("Symbolic interpolation result with interpolate_lookup (lists):", + symbolic_result_list.expr if isinstance(symbolic_result_list, MaterialProperty) else symbolic_result_list) numeric_result = interpolate_lookup(1894.7, density_temp_array2, density_v_array2) print("Numeric interpolation result with interpolate_lookup (arrays):", numeric_result) @@ -154,22 +204,22 @@ if __name__ == '__main__': # Interpolation Equidistant Tests print("\nInterpolate Equidistant Tests:") - symbolic_result_eq, ass1 = interpolate_equidistant(Temp, Temp_base, Temp_incr, density_v_array1, 'density') - print("Symbolic interpolation result with interpolate_equidistant (numpy arrays):", symbolic_result_eq) - print("Assignments:", ass1) + symbolic_result_eq = interpolate_equidistant(Temp, Temp_base, Temp_incr, density_v_array1) + print("Symbolic interpolation result with interpolate_equidistant (numpy arrays):", symbolic_result_eq.expr) + print("Assignments:", symbolic_result_eq.assignments) - symbolic_result_eq_list, ass2 = interpolate_equidistant(Temp, Temp_base, Temp_incr, density_v_array2, 'density') - print("Symbolic interpolation result with interpolate_equidistant (lists):", symbolic_result_eq_list) - print("Assignments:", ass2) + symbolic_result_eq_list = interpolate_equidistant(Temp, Temp_base, Temp_incr, density_v_array2) + print("Symbolic interpolation result with interpolate_equidistant (lists):", symbolic_result_eq_list.expr) + print("Assignments:", symbolic_result_eq_list.assignments) - numeric_result_eq, _ = interpolate_equidistant(1900.2, Temp_base, Temp_incr, density_v_array1, 'density') + numeric_result_eq = interpolate_equidistant(1900.2, Temp_base, Temp_incr, density_v_array1) print("Numeric interpolation result with interpolate_equidistant (numpy arrays):", numeric_result_eq) - numeric_result_eq_list, _ = interpolate_equidistant(1900.2, Temp_base, Temp_incr, density_v_array2, 'density') + numeric_result_eq_list = interpolate_equidistant(1900.2, Temp_base, Temp_incr, density_v_array2) print("Numeric interpolation result with interpolate_equidistant (lists):", numeric_result_eq_list) try: - error_result_eq = interpolate_equidistant("invalid", Temp_base, Temp_incr, density_v_array2, 'density') + error_result_eq = interpolate_equidistant("what's up", Temp_base, Temp_incr, density_v_array2) except ValueError as e: print("Error:", e) @@ -177,8 +227,9 @@ if __name__ == '__main__': print("\nConsistency Test between interpolate_lookup and interpolate_equidistant:") test_temps = [150.0, 200.0, 350.0, 450.0, 550.0, 650.0] for T in test_temps: - lookup_result = interpolate_lookup(T, np.array([600.0, 500.0, 400.0, 300.0, 200.0]), np.array([10.0, 20.0, 30.0, 40.0, 50.0])) - equidistant_result, _ = interpolate_equidistant(T, 600.0, -100.0, np.array([10.0, 20.0, 30.0, 40.0, 50.0]), 'density') + lookup_result = interpolate_lookup(T, np.flip(np.array([600.0, 500.0, 400.0, 300.0, 200.0])), + np.array([10.0, 20.0, 30.0, 40.0, 50.0])) + equidistant_result = interpolate_equidistant(T, 200.0, 100.0, np.array([10.0, 20.0, 30.0, 40.0, 50.0])) print(f"Temperature {T}:") print(f"interpolate_lookup result: {lookup_result}") print(f"interpolate_equidistant result: {equidistant_result}") @@ -189,23 +240,8 @@ if __name__ == '__main__': print(f"Skipping comparison for symbolic or non-numeric result at T = {T}") symbolic_lookup_result = interpolate_lookup(Temp, density_temp_array1, density_v_array1) - symbolic_equidistant_result, assignments = interpolate_equidistant(Temp, Temp_base, Temp_incr, density_v_array1, 'density') + symbolic_equidistant_result = interpolate_equidistant(Temp, Temp_base, Temp_incr, density_v_array1) print("\nSymbolic interpolation results for consistency check:") - print(f"interpolate_lookup (symbolic): {symbolic_lookup_result}") - print(f"interpolate_equidistant (symbolic): {symbolic_equidistant_result}") - print(f"interpolate_equidistant (symbolic): {assignments}") - - # Perform Interpolation Tests - # print("\nPerform Interpolation Tests:") - # T_test = 350.0 - # temp_array = np.array([500.0, 400.0, 300.0, 200.0]) - # prop_array = np.array([40.0, 30.0, 20.0, 10.0]) - # assignments = {} - - # result_numeric = interpolate_pro(T_test, temp_array, prop_array, 'property', assignments) - # print(f"Perform interpolation (numeric) result for T={T_test}: {result_numeric}") - - # T_symbolic = sp.Symbol('T') - # result_symbolic = perform_interpolation(T_symbolic, temp_array, prop_array, 'property', assignments) - # print(f"Perform interpolation (symbolic) result: {result_symbolic}") - # print(f"Assignments: {assignments}") + print(f"interpolate_lookup (symbolic): {symbolic_lookup_result.expr}") + print(f"interpolate_equidistant (symbolic): {symbolic_equidistant_result.expr}") + print(f"interpolate_equidistant (symbolic): {symbolic_equidistant_result.assignments}") diff --git a/src/materials/models.py b/src/materials/models.py index f342c0404bd0f650c2e284bcaa91ae79e8e0c8e0..8a453662e1dbb8dab6d23ddce7825f13c531182f 100644 --- a/src/materials/models.py +++ b/src/materials/models.py @@ -1,13 +1,14 @@ +import numpy as np import sympy as sp -from src.materials.typedefs import PropertyTypes, VariableTypes, ValueTypes +from typing import Union def density_by_thermal_expansion( - T: VariableTypes, - base_temperature: float, - base_density: float, - thermal_expansion_coefficient: PropertyTypes) \ - -> PropertyTypes: + temperature: Union[float, np.ndarray, sp.Expr], + temperature_base: float, + density_base: float, + thermal_expansion_coefficient: Union[float, np.ndarray, sp.Expr]) \ + -> Union[float, np.ndarray, sp.Expr]: """ Calculate density based on thermal expansion using the formula: rho(T) = rho_0 / (1 + tec * (T - T_0))^3 @@ -17,20 +18,22 @@ def density_by_thermal_expansion( - rho_0 is the density at the base temperature T_0. - tec is the thermal expansion coefficient. - :param T: Temperature value. Can be a numeric value (float) or a symbolic expression (sp.Symbol). - :param base_temperature: Reference temperature at which the base density is given, as a float. - :param base_density: Density at the reference temperature, as a float. - :param thermal_expansion_coefficient: Thermal expansion coefficient. Can be a numeric value (float) or a symbolic expression (sp.Expr). - :return: Density at temperature T. Returns a float if all inputs are numeric; otherwise, returns a sympy expression. + :param temperature: Temperature value. Can be a numeric value (float), numpy array (np.ndarray), or a symbolic expression (sp.Expr). + :param temperature_base: Reference temperature at which the base density is given, as a float. + :param density_base: Density at the reference temperature, as a float. + :param thermal_expansion_coefficient: Thermal expansion coefficient. Can be a numeric value (float), numpy array (np.ndarray), or a symbolic expression (sp.Expr). + :return: Density at temperature T. Returns a float if all inputs are numeric; otherwise, returns a numpy array or a sympy expression. """ - return base_density / (1 + thermal_expansion_coefficient * (T - base_temperature)) ** 3 + if isinstance(temperature, sp.Symbol) and isinstance(thermal_expansion_coefficient, np.ndarray): + raise ValueError("This combination is not valid. Provide the temperature data which is used for the thermal_expansion_coefficient and create a new interpolation material property.") + return density_base * (1 + thermal_expansion_coefficient * (temperature - temperature_base)) ** (-3) def thermal_diffusivity_by_heat_conductivity( - heat_conductivity: PropertyTypes, - density: PropertyTypes, - heat_capacity: PropertyTypes) \ - -> PropertyTypes: + heat_conductivity: Union[float, np.ndarray, sp.Expr], + density: Union[float, np.ndarray, sp.Expr], + heat_capacity: Union[float, np.ndarray, sp.Expr]) \ + -> Union[float, np.ndarray, sp.Expr]: """ Calculate thermal diffusivity using the formula: alpha(T) = k(T) / (rho(T) * c_p(T)) @@ -41,13 +44,60 @@ def thermal_diffusivity_by_heat_conductivity( - rho(T) is the density at temperature T. - c_p(T) is the specific heat capacity at temperature T. - :param heat_conductivity: Thermal conductivity. Can be a numeric value (float) or a symbolic expression (sp.Expr). - :param density: Density. Can be a numeric value (float) or a symbolic expression (sp.Expr). - :param heat_capacity: Specific heat capacity. Can be a numeric value (float) or a symbolic expression (sp.Expr). - :return: Thermal diffusivity. Returns a float if all inputs are numeric; otherwise, returns a sympy expression. + :param heat_conductivity: Thermal conductivity. Can be a numeric value (float), numpy array (np.ndarray), or a symbolic expression (sp.Expr). + :param density: Density. Can be a numeric value (float), numpy array (np.ndarray), or a symbolic expression (sp.Expr). + :param heat_capacity: Specific heat capacity. Can be a numeric value (float), numpy array (np.ndarray), or a symbolic expression (sp.Expr). + :return: Thermal diffusivity. Returns a float if all inputs are numeric; otherwise, returns a numpy array or a sympy expression. """ + if isinstance(density, sp.Piecewise) or isinstance(heat_conductivity, sp.Piecewise) or isinstance(heat_capacity, sp.Piecewise): + raise ValueError("Do not insert piecewise functions here. Prepare the data with corresponding temperature intervals.") result = heat_conductivity / (density * heat_capacity) - # Optional: Simplify the result if any input is an expression (sp.Expr) - # if isinstance(heat_conductivity, sp.Expr) or isinstance(density, sp.Expr) or isinstance(heat_capacity, sp.Expr): - # result = sp.simplify(result) return result + + +if __name__ == '__main__': + # Test 1: Single temperature value for density calculation + T_0 = 800 + T_1 = 1000 + tec = 1e-6 + rho = 8000 + print(f"Test 1 - Single temperature value: {T_1}, Density: {density_by_thermal_expansion(T_1, T_0, rho, tec)}") + # Expected output: Density value at T_1 = 1000 + + # Test 2: Temperature array for density calculation with constant TEC + T_a = np.linspace(1000, 2000, 3) + tec_a = np.ones(T_a.shape) * tec + print(f"Test 2a - Temperature array with constant TEC: {T_a}, Density: {density_by_thermal_expansion(T_a, T_0, rho, tec)}") + # Expected output: Array of densities for temperatures in T_a + + # Test 3: Temperature array for density calculation with array TEC + print(f"Test 2b - Temperature array with array TEC: {T_a}, Density: {density_by_thermal_expansion(T_a, T_0, rho, tec_a)}") + # Expected output: Array of densities with temperature-dependent TEC + + # Test 4: Thermal diffusivity calculation with scalar values + k = 30 + c_p = 600 + print(f"Test 3 - Thermal diffusivity with scalar values: {thermal_diffusivity_by_heat_conductivity(k, rho, c_p)}") + # Expected output: Scalar value of thermal diffusivity + + # Test 5: Thermal diffusivity calculation with array values for heat conductivity + k_a = np.linspace(30, 40, 3) + print(f"Test 4a - Thermal diffusivity with array of heat conductivity: {thermal_diffusivity_by_heat_conductivity(k_a, rho, c_p)}") + # Expected output: Array of thermal diffusivities + + # Test 6: Thermal diffusivity with density calculated by thermal expansion + calculated_densities = density_by_thermal_expansion(T_a, T_0, rho, tec) + print(f"Test 4b - Thermal diffusivity with calculated densities: {thermal_diffusivity_by_heat_conductivity(k_a, calculated_densities, c_p)}") + # Expected output: Array of thermal diffusivities considering temperature-dependent density + + # Test 7: Symbolic computation for density with sympy Symbol temperature + T_symbolic = sp.Symbol('T') + print(f"Test 5 - Symbolic density computation: {density_by_thermal_expansion(T_symbolic, T_0, rho, tec)}") + # Expected output: Sympy expression for density as a function of temperature + + # Test 8: Symbolic computation for thermal diffusivity + k_symbolic = sp.Symbol('k') + c_p_symbolic = sp.Symbol('c_p') + rho_symbolic = sp.Symbol('rho') + print(f"Test 6 - Symbolic thermal diffusivity computation: {thermal_diffusivity_by_heat_conductivity(k_symbolic, rho_symbolic, c_p_symbolic)}") + # Expected output: Sympy expression for thermal diffusivity as a function of k, rho, and c_p diff --git a/src/materials/typedefs.py b/src/materials/typedefs.py index 0fcd155cbcb4d3f8d62644af394c240f9370618d..7ba5ff70446c44c87c2cbf31ca62c22eae29ea84 100644 --- a/src/materials/typedefs.py +++ b/src/materials/typedefs.py @@ -1,30 +1,29 @@ """ typedefs.py -This module defines custom types and type aliases used throughout the materials module, enhancing -code readability and consistency. It includes data structures for assignments and mappings -between symbolic types and data types used in computations. +This module defines custom types, type aliases, and utility classes used throughout the materials module, +enhancing code readability and consistency. It includes data structures for assignments and the evaluation +of material properties as functions of symbolic variables and numerical values. Classes: Assignment: A dataclass representing an assignment operation with a left-hand side (lhs), right-hand side (rhs), and the type of the lhs. + MaterialProperty: A dataclass that represents a material property, which can be evaluated as a function + of a symbolic variable (e.g., temperature) and potentially includes symbolic assignments. Type Aliases: - ValueTypes: A type alias for numerical data types, which can be either numpy arrays or lists of floats. - PropertyTypes: A type alias for properties that can be either constant floats or sympy expressions. - VariableTypes: A type alias for variables that can be either floats or sympy symbols. - AssignmentType: A type alias for a dictionary where keys are strings representing labels and values are lists of - `Assignment` objects associated with those labels. + ArrayTypes: A type alias for numerical data types, which can be either numpy arrays, lists, or tuples. + PropertyTypes: A type alias for properties that can be either constant floats or MaterialProperty instances. Functions: - type_mapping(type_str: str) -> Union[np.dtype, Arr]: - Maps a string representation of a type to a corresponding numpy or pystencils data type. + type_mapping(type_str: str) -> Union[np.dtype, Arr]: Maps a string representation of a type to a corresponding + numpy or pystencils data type. """ import numpy as np import sympy as sp -from dataclasses import dataclass -from typing import Dict, List, Union +from dataclasses import dataclass, field +from typing import List, Union, Tuple, get_args @dataclass @@ -43,10 +42,156 @@ class Assignment: # Type aliases for various data types used in the project -ValueTypes = Union[np.ndarray, List[float]] # Numerical values can be represented as numpy arrays or lists of floats -PropertyTypes = Union[float, sp.Expr] # Property values can be either constant floats or sympy expressions -VariableTypes = Union[float, sp.Symbol] # Variables can be either numerical (floats) or symbolic (sympy symbols) +ArrayTypes = Union[np.ndarray, List, Tuple] # Numerical values can be represented as numpy arrays, lists, or tuples -# Type alias for a dictionary of assignments -# Dictionary where keys are labels (strings) and values are lists of `Assignment` objects -AssignmentType = Dict[str, List[Assignment]] + +@dataclass +class MaterialProperty: + """ + Represents a material property that can be evaluated as a function of a symbolic variable (e.g., temperature). + It can include symbolic assignments that are resolved during the evaluation. + + Attributes: + expr (sp.Expr): The symbolic expression representing the material property. + assignments (List[Assignment]): A list of Assignment instances representing any symbolic assignments that + need to be resolved during evaluation. + + Methods: + evalf(symbol: sp.Symbol, temperature: Union[float, ArrayTypes]) -> Union[float, np.ndarray]: + Evaluates the material property for a given temperature, resolving any symbolic assignments. + """ + expr: sp.Expr + assignments: List[Assignment] = field(default_factory=list) + + def evalf(self, symbol: sp.Symbol, temperature: Union[float, ArrayTypes]) -> Union[float, np.ndarray]: + """ + Evaluates the material property at specific temperature values. + + Args: + symbol (sp.Symbol): The symbolic variable (e.g., temperature) to substitute in the expression. + temperature (Union[float, ArrayTypes]): The temperature(s) at which to evaluate the property. + + Returns: + Union[float, np.ndarray]: The evaluated property value(s) at the given temperature(s). + """ + # If the expression has no symbolic variables, return it as a constant float + if not self.expr.free_symbols: + return float(self.expr) + + # If temperature is a numpy array, list, or tuple, evaluate the property for each temperature + if isinstance(temperature, get_args(ArrayTypes)): + if isinstance(temperature, np.ndarray): + temperature = temperature.astype(float) # Ensure all values are Python floats + return np.array([self.evalf(symbol, t) for t in temperature]) + + # Convert any numpy scalar to Python float + elif isinstance(temperature, np.generic): + temperature = float(temperature) + + # Prepare substitutions for symbolic assignments + substitutions = [(symbol, temperature)] + for assignment in self.assignments: + if isinstance(assignment.rhs, sp.Expr): + # Evaluate the right-hand side expression at the given temperature + value = sp.N(assignment.rhs.subs(symbol, temperature)) + value = int(value) if assignment.lhs_type == "int" else value + substitutions.append((assignment.lhs, value)) + else: + substitutions.append((assignment.lhs, assignment.rhs)) + + # Evaluate the material property with the substitutions + result = sp.N(self.expr.subs(substitutions)) + return float(result) + + +# Type alias for properties that can be either constant floats or MaterialProperty instances +PropertyTypes = Union[float, MaterialProperty] + +if __name__ == '__main__': + # Example usage and tests for MaterialProperty and Assignment classes + + T = sp.Symbol('T') + v = sp.IndexedBase('v') + i = sp.Symbol('i', type=sp.Integer) + + # Example 1: Constant material property + mp0 = MaterialProperty(sp.Float(405.)) + mp0.assignments.append(Assignment(sp.Symbol('A'), (100, 200), 'int')) + print(mp0) + print(mp0.evalf(T, 100.)) + + # Example 2: Linear temperature-dependent property + mp1 = MaterialProperty(T * 100.) + print(mp1) + print(mp1.evalf(T, 100.)) + + # Example 3: Indexed base with symbolic assignments + mp2 = MaterialProperty(v[i]) + mp2.assignments.append(Assignment(v, (3, 6, 9), 'float')) + mp2.assignments.append(Assignment(i, T / 100, 'int')) + print(mp2) + print(mp2.evalf(T, 97)) # Should evaluate with i=0 (since 97/100 is 0 when converted to int) + print(mp2.evalf(T, 107)) # Should evaluate with i=1 (since 107/100 is 1 when converted to int) + + # Example 4: Evaluate an expression with an array of temperatures + rho = 1e-6 * T ** 3 * 4000 + print(rho * np.array([10, 20, 30])) + +if __name__ == '__main__': + # Example usage and tests for MaterialProperty and Assignment classes + + T = sp.Symbol('T') + v = sp.IndexedBase('v') + i = sp.Symbol('i', type=sp.Integer) + + # Test 1: Constant material property + mp0 = MaterialProperty(sp.Float(405.)) + mp0.assignments.append(Assignment(sp.Symbol('A'), (100, 200), 'int')) + print(f"Test 1 - Constant property, no symbolic variables: {mp0.evalf(T, 100.)}") # Expected output: 405.0 + + # Test 2: Linear temperature-dependent property + mp1 = MaterialProperty(T * 100.) + print(f"Test 2 - Linear temperature-dependent property: {mp1.evalf(T, 100.)}") # Expected output: 10000.0 + + # Test 3: Indexed base with symbolic assignments + mp2 = MaterialProperty(v[i]) + mp2.assignments.append(Assignment(v, (3, 6, 9), 'float')) + mp2.assignments.append(Assignment(i, T / 100, 'int')) + print(f"Test 3a - Indexed base with i=0: {mp2.evalf(T, 97)}") # Expected output: 3 (i=0) + print(f"Test 3b - Indexed base with i=1: {mp2.evalf(T, 107)}") # Expected output: 6 (i=1) + + # Test 4: Evaluate an expression with an array of temperatures + rho = 1e-6 * T ** 3 * 4000 + temps = np.array([10, 20, 30]) + print(f"Test 4 - Expression evaluated with array: {rho * temps}") # Expected output: array of evaluated values + + # Additional Tests: + + # Test 5: Non-linear temperature-dependent property + mp3 = MaterialProperty(T ** 2 + T * 50 + 25) + print(f"Test 5 - Non-linear temperature-dependent property: {mp3.evalf(T, 10)}") # Expected output: 625.0 + + # Test 6: Temperature array evaluation for non-linear expression + print(f"Test 6 - Non-linear property with temperature array: {mp3.evalf(T, temps)}") + # Expected output: array of evaluated values for T in [10, 20, 30] + + # Test 7: Property with multiple symbolic assignments + mp4 = MaterialProperty(v[i] * T) + mp4.assignments.append(Assignment(v, (3, 6, 9), 'float')) + mp4.assignments.append(Assignment(i, T / 100, 'int')) + print(f"Test 7a - Property with multiple symbolic assignments at T=95: {mp4.evalf(T, 95)}") # Expected: 285.0 + print(f"Test 7b - Property with multiple symbolic assignments at T=205: {mp4.evalf(T, 205)}") # Expected: 1230.0 + + # Test 8: Constant property evaluated with temperature array + mp5 = MaterialProperty(sp.Float(500.)) + print(f"Test 8 - Constant property with temperature array: {mp5.evalf(T, temps)}") + # Expected output: array of 500s for each temperature + + # Test 9: Handling numpy scalar input + scalar_temp = np.float64(150.0) + mp6 = MaterialProperty(T + 50) + print(f"Test 9 - Handling numpy scalar input: {mp6.evalf(T, scalar_temp)}") # Expected output: 200.0 + + # Test 10: Property with no symbolic dependencies + mp7 = MaterialProperty(sp.Float(1500.)) + print(f"Test 10 - Property with no symbolic dependencies: {mp7.evalf(T, 200.)}") # Expected output: 1500.0