diff --git a/apps/HeatEquationKernelWithMaterial.py b/apps/HeatEquationKernelWithMaterial.py index 5a5929867231232d8c12d48ab1f12a8d7b90a5ce..8c489d185d099f635b6ed9e0e64087250d242819 100644 --- a/apps/HeatEquationKernelWithMaterial.py +++ b/apps/HeatEquationKernelWithMaterial.py @@ -23,7 +23,6 @@ with SourceFileGenerator() as sfg: yaml_path = files('pymatlib.data.alloys.SS316L').joinpath('SS304L.yaml') mat = create_alloy_from_yaml(yaml_path, u.center()) - # arr_container = DoubleLookupArrayContainer("SS316L", mat.temperature_array, mat.energy_density_array) arr_container = InterpolationArrayContainer.from_material("SS316L", mat) sfg.generate(arr_container) @@ -39,6 +38,5 @@ with SourceFileGenerator() as sfg: ps.Assignment(thermal_diffusivity_out.center(), thermal_diffusivity) ]) - # generate_sweep(ctx, 'HeatEquationKernelWithMaterial', ac, varying_parameters=((data_type, str(thermal_diffusivity)),)) sweep = Sweep("HeatEquationKernelWithMaterial", ac) sfg.generate(sweep) diff --git a/apps/walberla b/apps/walberla index 0aab9c0af2335b1f6fec75deae06e514ccb233ab..3a09f7b17ce244123214ed2675a54896786233d9 160000 --- a/apps/walberla +++ b/apps/walberla @@ -1 +1 @@ -Subproject commit 0aab9c0af2335b1f6fec75deae06e514ccb233ab +Subproject commit 3a09f7b17ce244123214ed2675a54896786233d9 diff --git a/src/pymatlib/core/data_handler.py b/src/pymatlib/core/data_handler.py index 0ce948709c0725c82c99614884d411c8c51886fa..8b71b33d159d1d900e42081b63aec8439f76b6ec 100644 --- a/src/pymatlib/core/data_handler.py +++ b/src/pymatlib/core/data_handler.py @@ -449,7 +449,7 @@ if __name__ == '__main__': # Example usage: # 1. Using a file path base_dir = Path(__file__).parent # Directory of the current file - _file_path = str( base_dir / '..' / 'data' / 'alloys' / 'SS316L' / 'density_temperature.txt' ) + _file_path = str( base_dir / '..' / 'data' / 'alloys' / 'SS304L' / 'density_temperature.txt' ) min_temp, max_temp = find_min_max_temperature(_file_path) print(f"Minimum Temperature from file: {min_temp}") print(f"Maximum Temperature from file: {max_temp}") diff --git a/src/pymatlib/core/interpolators.py b/src/pymatlib/core/interpolators.py index d00d314707b70b7537f47890bf59fb050b72d231..fc3a06de930daa65015bef3c79120cab994762fc 100644 --- a/src/pymatlib/core/interpolators.py +++ b/src/pymatlib/core/interpolators.py @@ -405,10 +405,13 @@ def prepare_interpolation_arrays(T_array: np.ndarray, E_array: np.ndarray) -> di # Basic validation if len(T_array) != len(E_array): - raise ValueError("Temperature and energy density arrays must have the same length") + raise ValueError(f"Energy density array (length {len(E_array)}) and temperature array (length {len(T_array)}) must have the same length!") - if len(T_array) < 2: - raise ValueError("Arrays must contain at least 2 elements") + if not E_array.size or not T_array.size: + raise ValueError("Energy density and temperature arrays must not be empty!") + + if len(E_array) < 2: + raise ValueError(f"Arrays must contain at least 2 elements, got {len(E_array)}!") # Check if arrays are monotonic T_increasing = np.all(np.diff(T_array) > 0) diff --git a/src/pymatlib/core/yaml_parser.py b/src/pymatlib/core/yaml_parser.py index d2889a36b2e054f1e5dd1ef001c9f8844db0a0eb..eab110edfe66e6074ad6a600933d4267b30b3187 100644 --- a/src/pymatlib/core/yaml_parser.py +++ b/src/pymatlib/core/yaml_parser.py @@ -12,9 +12,28 @@ from pymatlib.core.models import (density_by_thermal_expansion, from pymatlib.core.typedefs import MaterialProperty from ruamel.yaml import YAML, constructor, scanner from difflib import get_close_matches +from enum import Enum, auto + + +class PropertyType(Enum): + CONSTANT = auto() + FILE = auto() + KEY_VAL = auto() + COMPUTE = auto() + TUPLE_STRING = auto() + INVALID = auto() class MaterialConfigParser: + + ################################################## + # Class Constants and Attributes + ################################################## + + MIN_POINTS = 2 + EPSILON = 1e-10 # Small value to handle floating point comparisons + ABSOLUTE_ZERO = 0.0 # Kelvin + # Define valid properties as class-level constants VALID_PROPERTIES = { 'base_temperature', @@ -33,17 +52,20 @@ class MaterialConfigParser: 'latent_heat_of_vaporization', 'specific_enthalpy', 'surface_tension', + 'temperature', 'temperature_array', 'thermal_diffusivity', 'thermal_expansion_coefficient', } + ################################################## + # Initialization and YAML Loading + ################################################## + def __init__(self, yaml_path: str | Path) -> None: """Initialize parser with YAML file path. - Args: yaml_path: Path to the YAML configuration file - Raises: FileNotFoundError: If YAML file is not found ValueError: If configuration is invalid @@ -57,13 +79,15 @@ class MaterialConfigParser: def _load_yaml(self) -> Dict[str, Any]: """Load and parse YAML file. - Returns: Dict containing parsed YAML content + Raises: + FileNotFoundError: If YAML file is not found + constructor.DuplicateKeyError: If duplicate keys are found + scanner.ScannerError: If YAML syntax is invalid """ yaml = YAML(typ='safe') yaml.allow_duplicate_keys = False - try: with open(self.yaml_path, 'r') as f: return yaml.load(f) @@ -76,33 +100,45 @@ class MaterialConfigParser: except Exception as e: raise ValueError(f"Error parsing {self.yaml_path}: {str(e)}") + ################################################## + # Configuration Validation + ################################################## + def _validate_config(self) -> None: - """Validate YAML configuration structure and content.""" + """ + Validate YAML configuration structure and content. + This method checks the overall structure of the configuration, + validates property names, required fields, and property values. + Raises: + ValueError: If any part of the configuration is invalid. + """ if not isinstance(self.config, dict): # raise ValueError("Root YAML element must be a mapping") raise ValueError("The YAML file must start with a dictionary/object structure with key-value pairs, not a list or scalar value") - if 'properties' not in self.config: raise ValueError("Missing 'properties' section in configuration") - properties = self.config.get('properties', {}) if not isinstance(properties, dict): # raise ValueError("'properties' must be a mapping") raise ValueError("The 'properties' section in your YAML file must be a dictionary with key-value pairs") - self._validate_property_names(properties) self._validate_required_fields() self._validate_property_values(properties) def _validate_property_names(self, properties: Dict[str, Any]) -> None: - """Validate property names against allowed set.""" + """ + Validate property names against the allowed set. + Args: + properties (Dict[str, Any]): Dictionary of properties to validate. + Raises: + ValueError: If any property name is not in VALID_PROPERTIES. + """ invalid_props = set(properties.keys()) - self.VALID_PROPERTIES if invalid_props: suggestions = { prop: get_close_matches(prop, self.VALID_PROPERTIES, n=1, cutoff=0.6) for prop in invalid_props } - error_msg = "Invalid properties found:\n" for prop, matches in suggestions.items(): suggestion = f" (did you mean '{matches[0]}'?)" if matches else "" @@ -110,214 +146,543 @@ class MaterialConfigParser: raise ValueError(error_msg) def _validate_required_fields(self) -> None: - """Validate required configuration fields.""" + """ + Validate required configuration fields. + Raises: + ValueError: If any required field is missing. + """ required_fields = {'name', 'composition', 'solidus_temperature', 'liquidus_temperature'} missing_fields = required_fields - set(self.config.keys()) if missing_fields: raise ValueError(f"Missing required fields: {', '.join(missing_fields)}") - def _validate_property_values(self, properties: Dict[str, Any]) -> None: - """Validate property values for type and range constraints.""" - if 'density' in properties: - density = properties['density'] - if isinstance(density, (int, float)) and density <= 0: - raise ValueError("Density must be positive") - - if 'energy_density_temperature_array' in properties: - edta = properties['energy_density_temperature_array'] - if isinstance(edta, tuple) and len(edta) != 3: - raise ValueError("Temperature array must be a tuple of (start, end, points/step)") - -######################################################################################################################## + @staticmethod + def _validate_property_values(properties: Dict[str, Any]) -> None: + """ + Validate property values for type and range constraints. + Args: + properties (Dict[str, Any]): Dictionary of properties to validate. + Raises: + ValueError: If any property value is invalid. + """ + for prop_name, prop_value in properties.items(): + BASE_PROPERTIES = {'base_temperature', 'base_density'} + POSITIVE_PROPERTIES = {'density', 'heat_capacity', 'heat_conductivity', 'specific_enthalpy'} + NON_NEGATIVE_PROPERTIES = {'latent_heat'} + if prop_value is None or (isinstance(prop_value, str) and prop_value.strip() == ''): + raise ValueError(f"Property '{prop_name}' has an empty or undefined value") + if prop_name in BASE_PROPERTIES: + if not isinstance(prop_value, float) or prop_value <= 0: + raise ValueError(f"'{prop_name}' must be a positive number of type float, " + f"got {prop_value} of type {type(prop_value).__name__}") + if prop_name in POSITIVE_PROPERTIES: + if isinstance(prop_value, float) and prop_value <= 0: + raise ValueError(f"'{prop_name}' must be positive, got {prop_value}") + if prop_name in NON_NEGATIVE_PROPERTIES: + if isinstance(prop_value, float) and prop_value < 0: + raise ValueError(f"'{prop_name}' cannot be negative, got {prop_value}") + if prop_name == 'thermal_expansion_coefficient': + if isinstance(prop_value, float) and prop_value < -3e-5 or prop_value > 0.001: + raise ValueError(f"'{prop_name}' value {prop_value} is outside the expected range (-3e-5/K to 0.001/K)") + if prop_name == 'energy_density_temperature_array': + if not (isinstance(prop_value, str) and prop_value.startswith('(') and prop_value.endswith(')')): + raise ValueError(f"'{prop_name}' must be a tuple of three comma-separated values representing (start, end, points/step)") + if prop_name in ['energy_density_solidus', 'energy_density_liquidus']: + raise ValueError(f"{prop_name} cannot be set directly. It is computed from other properties") + + ################################################## + # Alloy Creation + ################################################## def create_alloy(self, T: Union[float, sp.Symbol]) -> Alloy: - """Creates Alloy instance from YAML configuration""" - alloy = Alloy( - elements=self._get_elements(), - composition=list(self.config['composition'].values()), - temperature_solidus=self.config['solidus_temperature'], - temperature_liquidus=self.config['liquidus_temperature'] - ) - self._process_properties(alloy, T) - return alloy + """ + Creates an Alloy instance from YAML configuration. + Args: + T (Union[float, sp.Symbol]): Temperature value or symbol. + Returns: + Alloy: An instance of the Alloy class. + Raises: + ValueError: If there's an error in creating the alloy. + """ + try: + alloy = Alloy( + elements=self._get_elements(), + composition=list(self.config['composition'].values()), + temperature_solidus=self.config['solidus_temperature'], + temperature_liquidus=self.config['liquidus_temperature'] + ) + self._process_properties(alloy, T) + return alloy + except KeyError as e: + raise ValueError(f"Configuration error: Missing {e}") + except Exception as e: + raise ValueError(f"Failed to create alloy: {e}") def _get_elements(self) -> List[ChemicalElement]: - """Convert element symbols to ChemicalElement instances""" + """ + Convert element symbols to ChemicalElement instances. + Returns: + List[ChemicalElement]: List of ChemicalElement instances. + Raises: + ValueError: If an invalid element symbol is encountered. + """ from pymatlib.data.element_data import element_map - return [element_map[sym] for sym in self.config['composition'].keys()] - - def _is_numeric(self, value: str) -> bool: - """Check if string represents a number (including scientific notation)""" + try: + return [element_map[sym] for sym in self.config['composition'].keys()] + except KeyError as e: + raise ValueError(f"Invalid element symbol: {e}") + + ################################################## + # Property Type Checking + ################################################## + @staticmethod + def _is_numeric(value: str) -> bool: + """ + Check if string represents a number (including scientific notation). + Args: + value (str): The string to check. + Returns: + bool: True if the string represents a number, False otherwise. + """ try: float(value) return True except ValueError: return False - def _is_data_file(self, value: Dict[str, str]) -> bool: - """Check if dictionary represents a valid data file configuration""" + @staticmethod + def _is_data_file(value: str | Dict[str, str]) -> bool: + """ + Check if the value represents a valid data file configuration. + Args: + value (Union[str, Dict[str, str]]): The value to check. + Returns: + bool: True if it's a valid data file configuration, False otherwise. + Raises: + ValueError: If the file configuration is invalid or contains extra keys. + """ # Simple format: property_name: "filename.txt" - if isinstance(value, str) and (value.endswith('.txt') or value.endswith('.csv') or value.endswith('.xlsx')): - return True + if isinstance(value, str): + return value.endswith(('.txt', '.csv', '.xlsx')) # Advanced format: property_name: { file: "filename", temp_col: "col1", prop_col: "col2" } - if isinstance(value, dict) and 'file' in value: - # Required keys for advanced format - required_keys = ['file', 'temp_col', 'prop_col'] - missing_keys = [k for k in required_keys if k not in value] - - if missing_keys: + if isinstance(value, dict) and 'file' in value and 'temp_col' in value and 'prop_col' in value: + required_keys = {'file', 'temp_col', 'prop_col'} + value_keys = set(value.keys()) + if not required_keys.issubset(value_keys): + missing_keys = required_keys - value_keys raise ValueError(f"Missing required keys for file configuration: {missing_keys}") + if value_keys != required_keys: + extra_keys = value_keys - required_keys + raise ValueError(f"Extra keys found in file configuration: {extra_keys}") return True return False - def _is_key_val_property(self, value: Dict) -> bool: - """Check if property is defined with key-val pairs""" - return isinstance(value, dict) and 'key' in value and 'val' in value + @staticmethod + def _is_key_val_property(value: Any) -> bool: + """ + Check if property is defined with key-val pairs. + Args: + value (Any): The value to check. + Returns: + bool: True if it's a key-val property, False otherwise. + Raises: + ValueError: If the key-val property configuration is invalid. + """ + if isinstance(value, dict) and 'key' in value and 'val' in value: + required_keys = {'key', 'val'} + value_keys = set(value.keys()) + + if value_keys != required_keys: + missing_keys = required_keys - value_keys + extra_keys = value_keys - required_keys + if missing_keys: + raise ValueError(f"Missing required keys for key-val property: {missing_keys}") + if extra_keys: + raise ValueError(f"Extra keys found in key-val property: {extra_keys}") + return True + return False - def _is_compute_property(self, value: Any) -> bool: - """Check if property should be computed using any valid format""" - # Simple format: property_name: compute + @staticmethod + def _is_compute_property(value: Any) -> bool: + """ + Check if property should be computed using any valid format. + Args: + value (Any): The value to check. + Returns: + bool: True if it's a compute property, False otherwise. + Raises: + ValueError: If the compute property configuration is invalid. + """ if isinstance(value, str) and value == 'compute': return True - # Advanced format: property_name: { compute: "method_name" } elif isinstance(value, dict) and 'compute' in value: - # Ensure no other keys are present - invalid_keys = [k for k in value.keys() if k != 'compute'] - if invalid_keys: - raise ValueError(f"Invalid keys in compute property: {invalid_keys}. Only 'compute' is allowed.") + required_keys = {'compute'} + value_keys = set(value.keys()) + + if value_keys != required_keys: + missing_keys = required_keys - value_keys + extra_keys = value_keys - required_keys + if missing_keys: + raise ValueError(f"Missing required key for compute property: {missing_keys}") + if extra_keys: + raise ValueError(f"Extra keys found in compute property: {extra_keys}") return True return False - def _process_properties(self, alloy: Alloy, T: Union[float, sp.Symbol]): - """Process all material properties in correct order""" - properties = self.config['properties'] - - # Step 1: Process constant float properties - for name, config in properties.items(): - if isinstance(config, (int, float)) or (isinstance(config, str) and self._is_numeric(config)): - self._process_constant_property(alloy, name, config) - - # Step 2: Process file-based properties - for name, config in properties.items(): - if self._is_data_file(config): - self._process_file_property(alloy, name, config, T) - - # Step 3: Process key-val pair properties - for name, config in properties.items(): - if self._is_key_val_property(config): - self._process_key_val_property(alloy, name, config, T) + def _determine_property_type(self, prop_name: str, config: Any) -> PropertyType: + """ + Determine the type of property based on its configuration. + Args: + prop_name (str): The name of the property. + config (Any): The configuration of the property. + Returns: + PropertyType: The determined property type. + """ + if isinstance(config, float) or (isinstance(config, str) and self._is_numeric(config)): + return PropertyType.CONSTANT + elif self._is_data_file(config): + return PropertyType.FILE + elif self._is_key_val_property(config): + return PropertyType.KEY_VAL + elif self._is_compute_property(config): + return PropertyType.COMPUTE + elif prop_name == 'energy_density_temperature_array' and isinstance(config, str) and config.startswith('(') and config.endswith(')'): + return PropertyType.TUPLE_STRING + else: + return PropertyType.INVALID - # Step 4: Process computed properties - for name, config in properties.items(): - if self._is_compute_property(config): - self._process_computed_property(alloy, name, T) + def _categorize_properties(self, properties: Dict[str, Any]) -> Dict[PropertyType, List[Tuple[str, Any]]]: + """ + Categorize properties based on their types. + Args: + properties (Dict[str, Any]): Dictionary of properties to categorize. + Returns: + Dict[PropertyType, List[Tuple[str, Any]]]: Categorized properties. + Raises: + ValueError: If an invalid property configuration is found. + """ + categorized_properties: Dict[PropertyType, List[Tuple[str, Any]]] = { + PropertyType.CONSTANT: [], + PropertyType.FILE: [], + PropertyType.KEY_VAL: [], + PropertyType.COMPUTE: [], + PropertyType.TUPLE_STRING: [] + } + for prop_name, config in properties.items(): + prop_type = self._determine_property_type(prop_name, config) + if prop_type == PropertyType.INVALID: + raise ValueError(f"Invalid configuration for property '{prop_name}': {config}") + categorized_properties[prop_type].append((prop_name, config)) + return categorized_properties + + ################################################## + # Property Processing + ################################################## + + def _process_properties(self, alloy: Alloy, T: Union[float, sp.Symbol]) -> None: + """ + Process all properties for the alloy. + Args: + alloy (Alloy): The alloy object to process properties for. + T (Union[float, sp.Symbol]): Temperature value or symbol. + Raises: + ValueError: If there's an error processing any property. + """ + properties = self.config['properties'] + try: + categorized_properties = self._categorize_properties(properties) + for prop_type, prop_list in categorized_properties.items(): + for prop_name, config in prop_list: + if prop_type == PropertyType.CONSTANT: + self._process_constant_property(alloy, prop_name, config) + elif prop_type == PropertyType.FILE: + self._process_file_property(alloy, prop_name, config, T) + elif prop_type == PropertyType.KEY_VAL: + self._process_key_val_property(alloy, prop_name, config, T) + elif prop_type == PropertyType.COMPUTE: + self._process_computed_property(alloy, prop_name, T) + elif prop_type == PropertyType.TUPLE_STRING: + # Handle tuple string properties if needed + pass + except Exception as e: + raise ValueError(f"Failed to process properties: {e}") ######################################################################################################################## - def _process_constant_property(self, alloy: Alloy, prop_name: str, prop_config: Union[int, float, str]): - """Process constant float property""" + @staticmethod + def _process_constant_property(alloy: Alloy, prop_name: str, prop_config: Union[int, float, str]) -> None: + """ + Process constant float property. + Args: + alloy (Alloy): The alloy object to update. + prop_name (str): The name of the property to set. + prop_config (Union[int, float, str]): The property value or string representation. + Raises: + ValueError: If the property value cannot be converted to float. + """ try: value = float(prop_config) setattr(alloy, prop_name, value) - except (ValueError, TypeError): - raise ValueError(f"Invalid number format for {prop_name}: {prop_config}") + except (ValueError, TypeError) as e: + error_msg = f"Invalid number format for {prop_name}: {prop_config}" + raise ValueError(error_msg) from e ######################################################################################################################## - def _process_file_property(self, alloy: Alloy, prop_name: str, file_config: Union[str, Dict], T: Union[float, sp.Symbol]): - """Process property data from file configuration""" - # Get the directory containing the YAML file - yaml_dir = self.base_dir - - # Construct path relative to YAML file location - if isinstance(file_config, dict) and 'file' in file_config: - file_config['file'] = str(yaml_dir / file_config['file']) - temp_array, prop_array = read_data_from_file(file_config) - else: - # For string configuration, construct the full path - file_path = str(yaml_dir / file_config) - temp_array, prop_array = read_data_from_file(file_path) - - # Store temperature array if not already set - if not hasattr(alloy, 'temperature_array') or len(alloy.temperature_array) == 0: - alloy.temperature_array = temp_array - - material_property = interpolate_property(T, temp_array, prop_array) - setattr(alloy, prop_name, material_property) - - # Store additional properties if this is energy_density - if prop_name == 'energy_density': - alloy.energy_density_temperature_array = temp_array - alloy.energy_density_array = prop_array - alloy.energy_density_solidus = material_property.evalf(T, alloy.temperature_solidus) - alloy.energy_density_liquidus = material_property.evalf(T, alloy.temperature_liquidus) + def _process_file_property(self, alloy: Alloy, prop_name: str, file_config: Union[str, Dict[str, Any]], T: Union[float, sp.Symbol]) -> None: + """ + Process property data from file configuration. + Args: + alloy (Alloy): The alloy object to update. + prop_name (str): The name of the property to set. + file_config (Union[str, Dict[str, Any]]): File path or configuration dictionary. + T (Union[float, sp.Symbol]): Temperature value or symbol. + Raises: + ValueError: If there's an error processing the file data. + """ + try: + # Get the directory containing the YAML file + yaml_dir = self.base_dir + # Construct path relative to YAML file location + if isinstance(file_config, dict) and 'file' in file_config: + file_config['file'] = str(yaml_dir / file_config['file']) + temp_array, prop_array = read_data_from_file(file_config) + else: + # For string configuration, construct the full path + file_path = str(yaml_dir / file_config) + temp_array, prop_array = read_data_from_file(file_path) + self._process_property_data(alloy, prop_name, T, temp_array, prop_array) + except Exception as e: + error_msg = f"Error processing file property {prop_name}: {str(e)}" + raise ValueError(error_msg) from e ######################################################################################################################## - def _process_key_val_property(self, alloy: Alloy, prop_name: str, prop_config: Dict, T: Union[float, sp.Symbol]): - """Process property defined with key-val pairs""" - key_array = self._process_key_definition(prop_config['key'], prop_config['val'], alloy) - val_array = np.array(prop_config['val'], dtype=float) - - if len(key_array) != len(val_array): - raise ValueError(f"Length mismatch in {prop_name}: key and val arrays must have same length") - - # Store temperature array if not already set - if not hasattr(alloy, 'temperature_array') or len(alloy.temperature_array) == 0: - alloy.temperature_array = key_array - - material_property = interpolate_property(T, key_array, val_array) - setattr(alloy, prop_name, material_property) - - # Store additional properties if this is energy_density - if prop_name == 'energy_density': - alloy.energy_density_temperature_array = key_array - alloy.energy_density_array = val_array - alloy.energy_density_solidus = material_property.evalf(T, alloy.temperature_solidus) - alloy.energy_density_liquidus = material_property.evalf(T, alloy.temperature_liquidus) + def _process_key_val_property(self, alloy: Alloy, prop_name: str, prop_config: Dict, T: Union[float, sp.Symbol]) -> None: + """ + Process property defined with key-val pairs. + Args: + alloy (Alloy): The alloy object to update. + prop_name (str): The name of the property to set. + prop_config (Dict[str, Any]): The property configuration dictionary. + T (Union[float, sp.Symbol]): Temperature value or symbol. + Raises: + ValueError: If there's an error processing the key-val property. + """ + try: + key_array = self._process_key_definition(prop_config['key'], prop_config['val'], alloy) + val_array = np.array(prop_config['val'], dtype=float) + if len(key_array) != len(val_array): + raise ValueError(f"Length mismatch in {prop_name}: key and val arrays must have same length") + self._process_property_data(alloy, prop_name, T, key_array, val_array) + except Exception as e: + error_msg = f"Error processing key-val property {prop_name}: {str(e)}" + raise ValueError(error_msg) from e def _process_key_definition(self, key_def, val_array, alloy: Alloy) -> np.ndarray: - """Process temperature key definition""" - if isinstance(key_def, str) and key_def.startswith('(') and key_def.endswith(')'): - return self._process_equidistant_key(key_def, len(val_array)) - elif isinstance(key_def, list): - return self._process_list_key(key_def, alloy) - else: - raise ValueError(f"Invalid key definition: {key_def}") + """ + Process temperature key definition. + Args: + key_def (Union[str, List[Union[str, float]]]): The key definition. + val_array (List[float]): The value array. + alloy (Alloy): The alloy object. + Returns: + np.ndarray: Processed key array. + Raises: + ValueError: If there's an error processing the key definition. + """ + try: + if isinstance(key_def, str) and key_def.startswith('(') and key_def.endswith(')'): + return self._process_equidistant_key(key_def, len(val_array)) + elif isinstance(key_def, list): + return self._process_list_key(key_def, alloy) + else: + raise ValueError(f"Invalid key definition: {key_def}") + except Exception as e: + error_msg = f"Error processing key definition: {str(e)}" + raise ValueError(error_msg) from e - def _process_equidistant_key(self, key_def: str, n_points: int) -> np.ndarray: - """Process equidistant key definition""" + @staticmethod + def _process_equidistant_key(key_def: str, n_points: int) -> np.ndarray: + """ + Process equidistant key definition. + Args: + key_def (str): The equidistant key definition string. + n_points (int): Number of points. + Returns: + np.ndarray: Equidistant key array. + Raises: + ValueError: If there's an error processing the equidistant key. + """ try: values = [float(x.strip()) for x in key_def.strip('()').split(',')] if len(values) != 2: raise ValueError("Equidistant definition must have exactly two values: (start, increment)") start, increment = values - return np.arange(start, start + increment * n_points, increment) - except ValueError as e: - raise ValueError(f"Invalid equidistant format: {key_def}. Error: {str(e)}") - - def _process_list_key(self, key_def: List, alloy: Alloy) -> np.ndarray: - """Process list key definition""" - processed_key = [] - for k in key_def: - if isinstance(k, str): - if k == 'solidus_temperature': - processed_key.append(alloy.temperature_solidus) - elif k == 'liquidus_temperature': - processed_key.append(alloy.temperature_liquidus) + key_array = np.arange(start, start + increment * n_points, increment) + return key_array + except Exception as e: + error_msg = f"Invalid equidistant format: {key_def}. Error: {str(e)}" + raise ValueError(error_msg) from e + + @staticmethod + def _process_list_key(key_def: List, alloy: Alloy) -> np.ndarray: + """ + Process list key definition. + Args: + key_def (List[Union[str, float]]): The list key definition. + alloy (Alloy): The alloy object. + Returns: + np.ndarray: Processed list key array. + Raises: + ValueError: If there's an error processing the list key. + """ + try: + processed_key = [] + for k in key_def: + if isinstance(k, str): + if k == 'solidus_temperature': + processed_key.append(alloy.temperature_solidus) + elif k == 'liquidus_temperature': + processed_key.append(alloy.temperature_liquidus) + else: + processed_key.append(float(k)) else: processed_key.append(float(k)) + key_array = np.array(processed_key, dtype=float) + return key_array + except Exception as e: + error_msg = f"Error processing list key: {str(e)}" + raise ValueError(error_msg) from e + + ################################################## + # Property Data Processing + ################################################## + + def _process_property_data(self, alloy: Alloy, prop_name: str, T: Union[float, sp.Symbol], temp_array: np.ndarray, prop_array: np.ndarray) -> None: + """ + Process property data and set it on the alloy object. + Args: + alloy (Alloy): The alloy object to update. + prop_name (str): The name of the property to set. + T (Union[float, sp.Symbol]): Temperature value or symbol. + temp_array (np.ndarray): Array of temperature values. + prop_array (np.ndarray): Array of property values. + Raises: + ValueError: If there's an error processing the property data. + """ + try: + if isinstance(T, sp.Symbol): + self._process_symbolic_temperature(alloy, prop_name, T, temp_array, prop_array) + elif isinstance(T, (float, int)): + self._process_constant_temperature(alloy, prop_name, T, temp_array, prop_array) else: - processed_key.append(float(k)) - return np.array(processed_key, dtype=float) + raise ValueError(f"Unexpected type for T: {type(T)}") + except Exception as e: + error_msg = f"Error processing property data for {prop_name}: {str(e)}" + raise ValueError(error_msg) from e -######################################################################################################################## + def _process_symbolic_temperature(self, alloy: Alloy, prop_name: str, T: sp.Symbol, temp_array: np.ndarray, prop_array: np.ndarray) -> None: + """ + Process property data for symbolic temperature. + Args: + alloy (Alloy): The alloy object to update. + prop_name (str): The name of the property to set. + T (sp.Symbol): Symbolic temperature. + temp_array (np.ndarray): Array of temperature values. + prop_array (np.ndarray): Array of property values. + """ + # If T is symbolic, store the full temperature array if not already set then interpolate + if getattr(alloy, 'temperature_array', None) is None or len(alloy.temperature_array) == 0: + alloy.temperature_array = temp_array + material_property = interpolate_property(T, temp_array, prop_array) + setattr(alloy, prop_name, material_property) + if prop_name == 'energy_density': + self._process_energy_density(alloy, material_property, T, temp_array, prop_array) - def _process_computed_property(self, alloy: Alloy, prop_name: str, T: Union[float, sp.Symbol]): - """Process computed properties using predefined models with dependency checking""" + @staticmethod + def _process_constant_temperature(alloy: Alloy, prop_name: str, T: Union[float, int], temp_array: np.ndarray, prop_array: np.ndarray) -> None: + """ + Process property data for constant temperature. + Args: + alloy (Alloy): The alloy object to update. + prop_name (str): The name of the property to set. + T (Union[float, int]): Constant temperature value. + temp_array (np.ndarray): Array of temperature values. + prop_array (np.ndarray): Array of property values. + """ + # If T is a constant, store just that value if not already set then interpolate + if getattr(alloy, 'temperature', None) is None: + alloy.temperature = float(T) + material_property = interpolate_property(T, temp_array, prop_array) + setattr(alloy, prop_name, material_property) - # Define property dependencies and their computation methods - computation_methods = { + @staticmethod + def _process_energy_density(alloy: Alloy, material_property: Any, T: sp.Symbol, temp_array: np.ndarray, prop_array: np.ndarray) -> None: + """ + Process additional properties for energy density. + Args: + alloy (Alloy): The alloy object to update. + material_property (Any): The interpolated material property. + T (sp.Symbol): Symbolic temperature. + temp_array (np.ndarray): Array of temperature values. + prop_array (np.ndarray): Array of property values. + """ + alloy.energy_density_temperature_array = temp_array + alloy.energy_density_array = prop_array + alloy.energy_density_solidus = material_property.evalf(T, alloy.temperature_solidus) + alloy.energy_density_liquidus = material_property.evalf(T, alloy.temperature_liquidus) + + ################################################## + # Computed Property Handling + ################################################## + + def _process_computed_property(self, alloy: Alloy, prop_name: str, T: Union[float, sp.Symbol]) -> None: + """ + Process computed properties using predefined models with dependency checking. + Args: + alloy (Alloy): The alloy object to process. + prop_name (str): The name of the property to compute. + T (Union[float, Symbol]): The temperature value or symbol. + Raises: + ValueError: If no computation method is defined for the property or if the method is unknown. + """ + computation_methods = self._get_computation_methods(alloy, T) + dependencies = self._get_dependencies() + # Check if property has computation methods + if prop_name not in computation_methods: + raise ValueError(f"No computation method defined for property: {prop_name}") + # Determine which computation method to use + prop_config = self.config['properties'][prop_name] + method = 'default' + if isinstance(prop_config, dict) and 'compute' in prop_config: + method = prop_config['compute'] + # Validate method exists + if method not in computation_methods[prop_name]: + available_methods = list(computation_methods[prop_name].keys()) + raise ValueError(f"Unknown computation method '{method}' for {prop_name}. Available: {available_methods}") + # Get dependencies for selected method + method_dependencies = dependencies[prop_name][method] + # Process dependencies + self._process_dependencies(alloy, prop_name, method_dependencies, T) + # Compute property + material_property = computation_methods[prop_name][method]() + setattr(alloy, prop_name, material_property) + # Handle special case for energy_density + if prop_name == 'energy_density' and isinstance(T, sp.Symbol): + self._handle_energy_density(alloy, material_property, T, method_dependencies) + + @staticmethod + def _get_computation_methods(alloy: Alloy, T: Union[float, sp.Symbol]): + """ + Get the computation methods for various properties of the alloy. + Args: + alloy (Alloy): The alloy object containing property data. + T (Union[float, sp.Symbol]): The temperature value or symbol. + Returns: + dict: A dictionary of computation methods for different properties. + """ + return { 'density': { 'default': lambda: density_by_thermal_expansion( T, @@ -348,12 +713,19 @@ class MaterialConfigParser: 'total_enthalpy': lambda: energy_density_total_enthalpy( alloy.density, alloy.specific_enthalpy - ) + ), }, } - # Define property dependencies for each computation method - dependencies = { + @staticmethod + def _get_dependencies(): + """ + Get the dependencies for each computation method. + Returns: + dict: A nested dictionary specifying the dependencies for each + computation method of each property. + """ + return { 'density': { 'default': ['base_temperature', 'base_density', 'thermal_expansion_coefficient'], }, @@ -367,94 +739,123 @@ class MaterialConfigParser: }, } - # Check if property has computation methods - if prop_name not in computation_methods: - raise ValueError(f"No computation method defined for property: {prop_name}") - - # Determine which computation method to use - prop_config = self.config['properties'][prop_name] - method = 'default' - - if isinstance(prop_config, dict) and 'compute' in prop_config: - method = prop_config['compute'] - - # Validate method exists - if method not in computation_methods[prop_name]: - available_methods = list(computation_methods[prop_name].keys()) - raise ValueError(f"Unknown computation method '{method}' for {prop_name}. Available: {available_methods}") - - # Get dependencies for selected method - method_dependencies = dependencies[prop_name][method] - - # Process dependencies first if they're marked for computation - if prop_name == 'energy_density': - if 'energy_density_temperature_array' not in self.config['properties']: - raise ValueError(f"energy_density_temperature_array must be defined when energy_density is computed") - - # Process energy_density_temperature_array - edta = self.config['properties']['energy_density_temperature_array'] - alloy.energy_density_temperature_array = self._process_edta(edta) - - # Process other dependencies - for dep in method_dependencies: - if hasattr(alloy, dep) and getattr(alloy, dep) is None: + def _process_dependencies(self, alloy: Alloy, prop_name: str, dependencies: List[str], T: Union[float, sp.Symbol]): + """ + Process and compute the dependencies required for a given property. + This method checks if each dependency is already computed for the alloy. + If not, it attempts to compute the dependency if a computation method is defined. + Args: + alloy (Alloy): The alloy object to process. + prop_name (str): The name of the property being computed. + dependencies (List[str]): List of dependency names for the property. + T (Union[float, sp.Symbol]): The temperature value or symbol. + Raises: + ValueError: If any required dependency cannot be computed or is missing. + """ + for dep in dependencies: + if getattr(alloy, dep, None) is None: if dep in self.config['properties']: dep_config = self.config['properties'][dep] if dep_config == 'compute' or (isinstance(dep_config, dict) and 'compute' in dep_config): self._process_computed_property(alloy, dep, T) - # Verify all dependencies are available - missing_deps = [dep for dep in method_dependencies - if not hasattr(alloy, dep) or getattr(alloy, dep) is None] + missing_deps = [dep for dep in dependencies if getattr(alloy, dep, None) is None] if missing_deps: raise ValueError(f"Cannot compute {prop_name}. Missing dependencies: {missing_deps}") - # Compute property - material_property = computation_methods[prop_name][method]() - setattr(alloy, prop_name, material_property) + def _handle_energy_density(self, alloy: Alloy, material_property: MaterialProperty, T: sp.Symbol, dependencies: List[str]): + """ + Handle the special case of energy density computation. + This method computes additional properties related to energy density when T is symbolic. + It computes the energy density array, solidus, and liquidus values. + Args: + alloy (Alloy): The alloy object to process. + material_property (MaterialProperty): The computed energy density property. + T (sp.Symbol): The symbolic temperature variable. + dependencies (List[str]): List of dependencies for energy density computation. + Raises: + ValueError: If T is not symbolic or if energy_density_temperature_array is not defined in the config. + """ + # Ensure T is symbolic + if not isinstance(T, sp.Symbol): + raise ValueError("_handle_energy_density should only be called with symbolic T") + # Check dependencies + deps_to_check = [getattr(alloy, dep) for dep in dependencies if hasattr(alloy, dep)] + if any(isinstance(dep, MaterialProperty) for dep in deps_to_check): + if 'energy_density_temperature_array' not in self.config['properties']: + raise ValueError(f"energy_density_temperature_array must be defined when energy_density is computed with symbolic T") + # Process energy_density_temperature_array + edta = self.config['properties']['energy_density_temperature_array'] + alloy.energy_density_temperature_array = self._process_edta(edta) + if len(alloy.energy_density_temperature_array) >= 2: + alloy.energy_density_array = material_property.evalf(T, alloy.energy_density_temperature_array) + alloy.energy_density_solidus = material_property.evalf(T, alloy.temperature_solidus) + alloy.energy_density_liquidus = material_property.evalf(T, alloy.temperature_liquidus) - # Handle special case for energy_density arrays and phase points - if prop_name == 'energy_density': - # Check if any dependency is a MaterialProperty - deps_to_check = [getattr(alloy, dep) for dep in method_dependencies if hasattr(alloy, dep)] - if any(isinstance(dep, MaterialProperty) for dep in deps_to_check): - if hasattr(alloy, 'energy_density_temperature_array') and len(alloy.energy_density_temperature_array) > 0: - alloy.energy_density_array = np.array([ - material_property.evalf(T, temp) for temp in alloy.energy_density_temperature_array - ]) - alloy.energy_density_solidus = material_property.evalf(T, alloy.temperature_solidus) - alloy.energy_density_liquidus = material_property.evalf(T, alloy.temperature_liquidus) + ################################################## + # Energy Density Temperature Array Processing + ################################################## def _process_edta(self, array_def: str) -> np.ndarray: - """Process temperature array definition with format (start, end, points/delta)""" + """ + Process temperature array definition with format (start, end, points/delta). + Args: + array_def (str): A string defining the temperature array in the format + "(start, end, points/delta)". + Returns: + np.ndarray: An array of temperature values. + Raises: + ValueError: If the array definition is invalid, improperly formatted, + or contains physically impossible temperatures. + Examples: + >>> self._process_edta("(300, 3000, 5)") + array([ 300., 975., 1650., 2325., 3000.]) + >>> self._process_edta("(3000, 300, -1350.)") + array([3000., 1650., 300.]) + """ if not (isinstance(array_def, str) and array_def.startswith('(') and array_def.endswith(')')): raise ValueError("Temperature array must be defined as (start, end, points/delta)") - try: # Parse the tuple string values = [v.strip() for v in array_def.strip('()').split(',')] if len(values) != 3: - raise ValueError("Temperature array definition must have exactly three values") - - start = float(values[0]) - end = float(values[1]) - step = values[2] - + raise ValueError("'energy_density_temperature_array' must be a tuple of three comma-separated values representing (start, end, points/step)") + start, end, step = float(values[0]), float(values[1]), values[2] + if start <= self.ABSOLUTE_ZERO or end <= self.ABSOLUTE_ZERO: + raise ValueError(f"Temperature must be above absolute zero ({self.ABSOLUTE_ZERO}K)") + if abs(float(step)) < self.EPSILON: + raise ValueError("Delta or number of points cannot be zero.") # Check if step represents delta (float) or points (int) if '.' in step or 'e' in step.lower(): - delta = float(step) - return np.arange(start, end + delta/2, delta) # delta/2 ensures end point inclusion + return self._process_float_step(start, end, float(step)) else: - # Handle as points (int) - points = int(step) - if points <= 0: - raise ValueError(f"Number of points must be a positive integer, got {points}") - return np.linspace(start, end, points) - + return self._process_int_step(start, end, int(step)) except ValueError as e: raise ValueError(f"Invalid temperature array definition: {e}") -######################################################################################################################## + @staticmethod + def _process_float_step(start: float, end: float, delta: float) -> np.ndarray: + """Process temperature array with float step (delta).""" + if start < end and delta <= 0: + raise ValueError("Delta must be positive for increasing range") + if start > end and delta >= 0: + raise ValueError("Delta must be negative for decreasing range") + max_delta = abs(end - start) + if abs(delta) > max_delta: + raise ValueError(f"Absolute value of delta ({abs(delta)}) is too large for the range. It should be <= {max_delta}") + return np.arange(start, end + delta/2, delta) + + def _process_int_step(self, start: float, end: float, points: int) -> np.ndarray: + """Process temperature array with integer step (number of points).""" + if points <= 0: + raise ValueError(f"Number of points must be positive, got {points}!") + if points < self.MIN_POINTS: + raise ValueError(f"Number of points must be at least {self.MIN_POINTS}, got {points}!") + return np.linspace(start, end, points) + +################################################## +# External Function +################################################## def create_alloy_from_yaml(yaml_path: Union[str, Path], T: Union[float, sp.Symbol]) -> Alloy: """Create alloy instance from YAML configuration file""" diff --git a/src/pymatlib/data/alloys/SS316L/304L_Erstarrungsdaten_edited.xlsx b/src/pymatlib/data/alloys/SS304L/304L_Erstarrungsdaten_edited.xlsx similarity index 100% rename from src/pymatlib/data/alloys/SS316L/304L_Erstarrungsdaten_edited.xlsx rename to src/pymatlib/data/alloys/SS304L/304L_Erstarrungsdaten_edited.xlsx diff --git a/src/pymatlib/data/alloys/SS316L/SS316L.py b/src/pymatlib/data/alloys/SS304L/SS304L.py similarity index 81% rename from src/pymatlib/data/alloys/SS316L/SS316L.py rename to src/pymatlib/data/alloys/SS304L/SS304L.py index 86de8aaa5025b51dbdb960d70aff1fe05cfd40a9..7483217f8a39de102dbf4b81ec1b4ff67e3bfd79 100644 --- a/src/pymatlib/data/alloys/SS316L/SS316L.py +++ b/src/pymatlib/data/alloys/SS304L/SS304L.py @@ -3,7 +3,6 @@ import numpy as np import sympy as sp from pathlib import Path from typing import Union -from matplotlib import pyplot as plt from pymatlib.core.alloy import Alloy from pymatlib.data.element_data import Fe, Cr, Ni, Mo, Mn from pymatlib.core.models import thermal_diffusivity_by_heat_conductivity, density_by_thermal_expansion, energy_density, energy_density_total_enthalpy @@ -11,13 +10,13 @@ from pymatlib.core.data_handler import read_data_from_txt, celsius_to_kelvin, th from pymatlib.core.interpolators import interpolate_property, prepare_interpolation_arrays, interpolate_binary_search, interpolate_double_lookup -def create_SS316L(T: Union[float, sp.Symbol]) -> Alloy: +def create_SS304L(T: Union[float, sp.Symbol]) -> Alloy: """ - Creates an Alloy instance for SS316L stainless steel with specific properties. + Creates an Alloy instance for SS304L 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. + Alloy: Initialized SS304L alloy with physical properties. Notes: - **Material Properties**: - **Density**: 8.0 g/cm³ (8000 kg/m³) at room temperature @@ -45,18 +44,18 @@ def create_SS316L(T: Union[float, sp.Symbol]) -> Alloy: - heat_conductivity_temperature.txt Example: >>> T = sp.Symbol('T') - >>> ss316l = create_SS316L(T) + >>> ss316l = create_SS304L(T) >>> density_at_1000K = ss316l.density.evalf(T, 1000.0) """ # Define the alloy with specific elemental composition and phase transition temperatures - SS316L = Alloy( + SS304L = Alloy( elements=[Fe, Cr, Ni, Mo, Mn], composition=[0.675, 0.17, 0.12, 0.025, 0.01], # Fe: 67.5%, Cr: 17%, Ni: 12%, Mo: 2.5%, Mn: 1% temperature_solidus=1605., # Solidus temperature in Kelvin (test at 1653.15 K = 1380 C) temperature_liquidus=1735., # Liquidus temperature in Kelvin (test at 1723.15 K = 1450 C) thermal_expansion_coefficient=16.3e-6 # in 1/K ) - # density_data_file_path = "/local/ca00xebo/repos/pymatlib/src/pymatlib/data/alloys/SS316L/density_temperature.txt" + # density_data_file_path = "/local/ca00xebo/repos/pymatlib/src/pymatlib/data/alloys/SS304L/density_temperature.txt" # Determine the base directory base_dir = Path(__file__).parent # Directory of the current file @@ -65,8 +64,8 @@ def create_SS316L(T: Union[float, sp.Symbol]) -> Alloy: density_data_file_path = str(base_dir / '304L_Erstarrungsdaten_edited.xlsx') # heat_capacity_data_file_path = str(base_dir / 'heat_capacity_temperature_edited.txt') heat_capacity_data_file_path = str(base_dir / '304L_Erstarrungsdaten_edited.xlsx') - heat_conductivity_data_file_path = str(base_dir / '..' / 'SS316L' / '304L_Erstarrungsdaten_edited.xlsx') - latent_heat_of_fusion_data_file_path = str(base_dir / '..' / 'SS316L' / '304L_Erstarrungsdaten_edited.xlsx') + heat_conductivity_data_file_path = str(base_dir / '..' / 'SS304L' / '304L_Erstarrungsdaten_edited.xlsx') + latent_heat_of_fusion_data_file_path = str(base_dir / '..' / 'SS304L' / '304L_Erstarrungsdaten_edited.xlsx') # Read temperature and material property data from the files # density_temp_array, density_array = read_data_from_txt(density_data_file_path) @@ -86,33 +85,33 @@ def create_SS316L(T: Union[float, sp.Symbol]) -> Alloy: heat_conductivity_temp_array, heat_conductivity_array]): raise ValueError("Failed to load temperature or material data from the file.") - SS316L.heat_conductivity = interpolate_property(T, heat_conductivity_temp_array, heat_conductivity_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.thermal_diffusivity = thermal_diffusivity_by_heat_conductivity(SS316L.heat_conductivity, SS316L.density, SS316L.heat_capacity) - # SS316L.latent_heat_of_fusion = interpolate_property(T, SS316L.solidification_interval(), np.array([171401.0, 0.0])) - SS316L.latent_heat_of_fusion = interpolate_property(T, latent_heat_of_fusion_temp_array, latent_heat_of_fusion_array) - SS316L.specific_enthalpy = interpolate_property(T, density_temp_array, sp_enthalpy_array) - SS316L.energy_density = energy_density_total_enthalpy(SS316L.density, SS316L.specific_enthalpy) - SS316L.energy_density_solidus = SS316L.energy_density.evalf(T, SS316L.temperature_solidus) - SS316L.energy_density_liquidus = SS316L.energy_density.evalf(T, SS316L.temperature_liquidus) + SS304L.heat_conductivity = interpolate_property(T, heat_conductivity_temp_array, heat_conductivity_array) + SS304L.density = interpolate_property(T, density_temp_array, density_array) + SS304L.heat_capacity = interpolate_property(T, heat_capacity_temp_array, heat_capacity_array) + SS304L.thermal_diffusivity = thermal_diffusivity_by_heat_conductivity(SS304L.heat_conductivity, SS304L.density, SS304L.heat_capacity) + # SS304L.latent_heat_of_fusion = interpolate_property(T, SS304L.solidification_interval(), np.array([171401.0, 0.0])) + SS304L.latent_heat_of_fusion = interpolate_property(T, latent_heat_of_fusion_temp_array, latent_heat_of_fusion_array) + SS304L.specific_enthalpy = interpolate_property(T, density_temp_array, sp_enthalpy_array) + SS304L.energy_density = energy_density_total_enthalpy(SS304L.density, SS304L.specific_enthalpy) + SS304L.energy_density_solidus = SS304L.energy_density.evalf(T, SS304L.temperature_solidus) + SS304L.energy_density_liquidus = SS304L.energy_density.evalf(T, SS304L.temperature_liquidus) # Populate temperature_array and energy_density_array - SS316L.temperature_array = density_temp_array - SS316L.energy_density_temperature_array = u1_temp_array + SS304L.temperature_array = density_temp_array + SS304L.energy_density_temperature_array = u1_temp_array - SS316L.energy_density_array = np.array([ - SS316L.energy_density.evalf(T, temp) for temp in density_temp_array + SS304L.energy_density_array = np.array([ + SS304L.energy_density.evalf(T, temp) for temp in density_temp_array ]) - args = (SS316L.temperature_array, - SS316L.energy_density_liquidus, - SS316L.energy_density_array) + args = (SS304L.temperature_array, + SS304L.energy_density_liquidus, + SS304L.energy_density_array) - E = SS316L.energy_density.evalf(T, SS316L.temperature_liquidus) + E = SS304L.energy_density.evalf(T, SS304L.temperature_liquidus) result = prepare_interpolation_arrays( - SS316L.temperature_array, - SS316L.energy_density_array + SS304L.temperature_array, + SS304L.energy_density_array ) if result["method"] == "double_lookup": @@ -144,18 +143,18 @@ def create_SS316L(T: Union[float, sp.Symbol]) -> Alloy: T_star = interpolate_binary_search(*args) print(f"T_star: {T_star}") - return SS316L + return SS304L if __name__ == '__main__': Temp = sp.Symbol('T') - alloy = create_SS316L(Temp) + alloy = create_SS304L(Temp) # Print the composition of each element in the alloy for i in range(len(alloy.composition)): print(f"Element {alloy.elements[i]}: {alloy.composition[i]}") - print("\nTesting SS316L with symbolic temperature:") + print("\nTesting SS304L with symbolic temperature:") for field in vars(alloy): print(f"{field} = {alloy.__getattribute__(field)}") diff --git a/src/pymatlib/data/alloys/SS316L/SS304L.yaml b/src/pymatlib/data/alloys/SS304L/SS304L.yaml similarity index 92% rename from src/pymatlib/data/alloys/SS316L/SS304L.yaml rename to src/pymatlib/data/alloys/SS304L/SS304L.yaml index c75fead142376d0ab0f99fdcc971a870196b42b5..8af978baa2616dfc4d0d9f52d5511f1f930e7146 100644 --- a/src/pymatlib/data/alloys/SS316L/SS304L.yaml +++ b/src/pymatlib/data/alloys/SS304L/SS304L.yaml @@ -133,8 +133,8 @@ properties: #OR # energy_density: compute #OR - energy_density: - compute: total_enthalpy # Explicit model selection + energy_density: compute + #compute: total_enthalpy # Explicit model selection # User can specify either: energy_density_temperature_array: (300, 3000, 541) # int for number of points # OR @@ -142,22 +142,16 @@ properties: # save the energy_density and temperature as arrays always. - #density: 7950 + #density: 7950. #OR - #density: compute # computed by thermal expansion coefficient, should be acceptable even if TEC is defined later in the file - base_temperature: 2273 + density: compute # computed by thermal expansion coefficient, should be acceptable even if TEC is defined later in the file + base_temperature: 2273. base_density: 6.591878918e3 #OR - density: - file: ./304L_Erstarrungsdaten_edited.xlsx - temp_col: T (K) - prop_col: Density (kg/(m)^3) - heat_conductivity: - file: ./304L_Erstarrungsdaten_edited.xlsx - temp_col: T (K) - prop_col: Thermal conductivity (W/(m*K))-TOTAL-10000.0(K/s) + + heat_capacity: @@ -182,9 +176,9 @@ properties: # val: [171401, 0] - # heat_conductivity: - # key: [1200, 1800, 2200, 2400] # temperature values - # val: [25, 30, 33, 35] # corresponding heat_conductivity values + heat_conductivity: + val: [25, 30, 33, 35] # corresponding heat_conductivity values + key: [25, 30, 33, 35] # heat_capacity: diff --git a/src/pymatlib/data/alloys/SS316L/__init__.py b/src/pymatlib/data/alloys/SS304L/__init__.py similarity index 100% rename from src/pymatlib/data/alloys/SS316L/__init__.py rename to src/pymatlib/data/alloys/SS304L/__init__.py diff --git a/src/pymatlib/data/alloys/SS316L/density_temperature.txt b/src/pymatlib/data/alloys/SS304L/density_temperature.txt similarity index 100% rename from src/pymatlib/data/alloys/SS316L/density_temperature.txt rename to src/pymatlib/data/alloys/SS304L/density_temperature.txt diff --git a/src/pymatlib/data/alloys/SS316L/density_temperature_edited.txt b/src/pymatlib/data/alloys/SS304L/density_temperature_edited.txt similarity index 100% rename from src/pymatlib/data/alloys/SS316L/density_temperature_edited.txt rename to src/pymatlib/data/alloys/SS304L/density_temperature_edited.txt diff --git a/src/pymatlib/data/alloys/SS316L/heat_capacity_temperature.txt b/src/pymatlib/data/alloys/SS304L/heat_capacity_temperature.txt similarity index 100% rename from src/pymatlib/data/alloys/SS316L/heat_capacity_temperature.txt rename to src/pymatlib/data/alloys/SS304L/heat_capacity_temperature.txt diff --git a/src/pymatlib/data/alloys/SS316L/heat_capacity_temperature_edited.txt b/src/pymatlib/data/alloys/SS304L/heat_capacity_temperature_edited.txt similarity index 100% rename from src/pymatlib/data/alloys/SS316L/heat_capacity_temperature_edited.txt rename to src/pymatlib/data/alloys/SS304L/heat_capacity_temperature_edited.txt diff --git a/src/pymatlib/data/alloys/SS316L/heat_conductivity_temperature.txt b/src/pymatlib/data/alloys/SS304L/heat_conductivity_temperature.txt similarity index 100% rename from src/pymatlib/data/alloys/SS316L/heat_conductivity_temperature.txt rename to src/pymatlib/data/alloys/SS304L/heat_conductivity_temperature.txt diff --git a/tests/cpp/TestArrayContainer.py b/tests/cpp/TestArrayContainer.py index a20d260637c211345334af2fed0b3be8d708f24d..8fc49760bbba61743ae2cabc84cde4b6a7d3c39f 100644 --- a/tests/cpp/TestArrayContainer.py +++ b/tests/cpp/TestArrayContainer.py @@ -21,7 +21,7 @@ with SourceFileGenerator() as sfg: custom_container = InterpolationArrayContainer("DoubleLookupTests", np.flip(T_eq), np.flip(E_neq)) sfg.generate(custom_container) - yaml_path = files('pymatlib.data.alloys.SS316L').joinpath('SS304L.yaml') + yaml_path = files('pymatlib.data.alloys.SS304L').joinpath('SS304L.yaml') mat = create_alloy_from_yaml(yaml_path, u.center()) - arr_container = InterpolationArrayContainer.from_material("SS316L", mat) + arr_container = InterpolationArrayContainer.from_material("SS304L", mat) sfg.generate(arr_container) diff --git a/tests/legacy/test_interpolate_functs_perf.py b/tests/legacy/test_interpolate_functs_perf.py index 4cb35850857fa8e3785bfb5229bf32687dafefbe..793570b3d7fa0f56ffa399b3a1073d77487f3073 100644 --- a/tests/legacy/test_interpolate_functs_perf.py +++ b/tests/legacy/test_interpolate_functs_perf.py @@ -97,8 +97,8 @@ def create_alloy(T: Union[float, sp.Symbol]) -> Alloy: base_dir = Path(__file__).parent # Paths to data files using relative paths - density_data_file_path = str(base_dir/'..'/'data'/'alloys'/'SS316L'/'density_temperature_edited.txt') - heat_capacity_data_file_path = str(base_dir/'..'/'data'/'alloys'/'SS316L'/'heat_capacity_temperature_edited.txt') + density_data_file_path = str(base_dir/'..'/'data'/'alloys'/'SS304L'/'density_temperature_edited.txt') + heat_capacity_data_file_path = str(base_dir/'..'/'data'/'alloys'/'SS304L'/'heat_capacity_temperature_edited.txt') # Read temperature and material property data from the files density_temp_array, density_array = read_data(density_data_file_path) @@ -162,7 +162,7 @@ def create_alloy(T: Union[float, sp.Symbol]) -> Alloy: raise ValueError(f"Mismatch value. Temperature value should be {alloy.temperature_liquidus}") E_target_alloy = generate_target_points(float(alloy.energy_density_array[0]), float(alloy.energy_density_array[-1]), 1_000) - compare_interpolation_methods(E_target_alloy, T_eq, E_neq, E_eq, inv_delta_E_eq, idx_map, 'SS316L') + compare_interpolation_methods(E_target_alloy, T_eq, E_neq, E_eq, inv_delta_E_eq, idx_map, 'SS304L') def measure_performance(iterations=1): all_execution_times = np.zeros(iterations) diff --git a/tests/python/test_alloy.py b/tests/python/test_alloy.py index 8477a9517042c229795d9813c2eb5dca02984e6a..cfbb44cfe278a952fa659fe4d5ee29b765025e56 100644 --- a/tests/python/test_alloy.py +++ b/tests/python/test_alloy.py @@ -3,7 +3,7 @@ import numpy as np import sympy as sp from pymatlib.core.alloy import Alloy, AlloyCompositionError, AlloyTemperatureError from pymatlib.data.element_data import Ti, Al, V, Fe, Cr, Mn, Ni -from src.pymatlib.data.alloys.SS316L.SS316L import create_SS316L +from src.pymatlib.data.alloys.SS304L.SS304L import create_SS304L from src.pymatlib.core.typedefs import MaterialProperty def test_alloy_creation(): @@ -22,9 +22,9 @@ def test_alloy_creation(): Alloy(elements=[Fe, Cr, Mn, Ni], composition=[0.7, 0.2, 0.05, 0.05], temperature_solidus=1900., temperature_liquidus=1800.) def test_create_SS316L(): - """Test the creation and properties of SS316L alloy.""" + """Test the creation and properties of SS304L alloy.""" # Test with float temperature input - alloy = create_SS316L(1400.0) + alloy = create_SS304L(1400.0) # Check if properties are set and have correct types assert hasattr(alloy, 'density') @@ -40,7 +40,7 @@ def test_create_SS316L(): # Test with symbolic temperature input T = sp.Symbol('T') - alloy_symbolic = create_SS316L(T) + alloy_symbolic = create_SS304L(T) # Check symbolic properties assert alloy_symbolic.density is not None @@ -55,7 +55,7 @@ def test_create_SS316L(): assert isinstance(float(alloy.thermal_diffusivity.expr), float) def test_create_SS316L2(): - alloy = create_SS316L(1400.0) + alloy = create_SS304L(1400.0) # Check if density exists and has the correct type assert hasattr(alloy, 'density') @@ -70,14 +70,14 @@ def test_alloy_single_element(): def test_alloy_property_modification(): # Test accessing and modifying individual alloy properties - alloy = create_SS316L(1400.0) + alloy = create_SS304L(1400.0) # assert isinstance(alloy.density, MaterialProperty) # Set the density to a MaterialProperty instance with a constant expression alloy.density = MaterialProperty(expr=sp.Float(8000.0)) assert alloy.density.expr == sp.Float(8000.0) '''def test_create_SS316L_invalid_temperature(): - # Test creating SS316L alloy with invalid temperature inputs + # Test creating SS304L alloy with invalid temperature inputs with pytest.raises(ValueError): create_SS316L(-100.0) # Negative temperature with pytest.raises(ValueError): diff --git a/tests/python/test_yaml_config.py b/tests/python/test_yaml_config.py index d3709938f4a8979fe083d08467fae474c1986405..f121e0c10456047b460f66c5e8de679d910082fb 100644 --- a/tests/python/test_yaml_config.py +++ b/tests/python/test_yaml_config.py @@ -14,11 +14,10 @@ T = sp.Symbol('T') # Get the path to the YAML file current_file = Path(__file__) -yaml_path = current_file.parent.parent.parent / "src" / "pymatlib" / "data" / "alloys" / "SS316L" / "SS304L.yaml" +yaml_path = current_file.parent.parent.parent / "src" / "pymatlib" / "data" / "alloys" / "SS304L" / "SS304L.yaml" # Create alloy from YAML ss316l = create_alloy_from_yaml(yaml_path, T) -#ss316l_1 = create_alloy_from_yaml("SS304L_1.yaml", T) # Test various properties print(f"Elements: {ss316l.elements}") @@ -26,14 +25,16 @@ print(f"Composition: {ss316l.composition}") # Print the composition of each element in the alloy for i in range(len(ss316l.composition)): print(f"Element {ss316l.elements[i]}: Composition {ss316l.composition[i]}") -print(f"Solidus Temperature: {ss316l.temperature_solidus}") + +print(f"\nSolidus Temperature: {ss316l.temperature_solidus}") print(f"Liquidus Temperature: {ss316l.temperature_liquidus}") -print("\nTesting SS316L with symbolic temperature:") + +print("\nTesting SS304L with symbolic temperature:") for field in vars(ss316l): print(f"{field} = {ss316l.__getattribute__(field)}") # Test computed properties at specific temperature -test_temp = 1605 +test_temp = 1670 print(f"\nProperties at {test_temp}K:") print(f"Density: {print_property_value(ss316l.density, T, test_temp)}") print(f"Specific enthalpy: {print_property_value(ss316l.specific_enthalpy, T, test_temp)}") @@ -46,4 +47,4 @@ print(f"Latent heat: {ss316l.latent_heat_of_fusion.evalf(T, test_temp)}") # Test array generation for energy density if hasattr(ss316l, 'energy_density_array'): print(f"\nEnergy Density Array Shape: {ss316l.energy_density_array.shape}") - print(f"Temperature Array Shape: {ss316l.energy_density_temperature_array.shape}") + print(f"Energy Density Temperature Array Shape: {ss316l.energy_density_temperature_array.shape}")