From bdb75b2b03cf0ae26ff883291e34696fd84a953e Mon Sep 17 00:00:00 2001 From: Rahil Doshi <rahil.doshi@fau.de> Date: Mon, 17 Mar 2025 17:58:11 +0100 Subject: [PATCH] Improve error handling and parsing logic --- src/pymatlib/core/data_handler.py | 4 +- src/pymatlib/core/yaml_parser.py | 320 ++++++++++++++++++++---------- 2 files changed, 215 insertions(+), 109 deletions(-) diff --git a/src/pymatlib/core/data_handler.py b/src/pymatlib/core/data_handler.py index 5d631e5..3d741a0 100644 --- a/src/pymatlib/core/data_handler.py +++ b/src/pymatlib/core/data_handler.py @@ -357,7 +357,7 @@ def read_data_from_file(file_config: Union[str, Dict], header: bool = True) -> T temp = df[temp_col].to_numpy(dtype=np.float64) else: raise ValueError(f"Temperature column '{temp_col}' not found in file. " - f"Available columns: {', '.join(df.columns)}") + f"\n -> Available columns: {', '.join(df.columns)}") else: if temp_col >= len(df.columns): raise ValueError(f"Temperature column index {temp_col} out of bounds (file has {len(df.columns)} columns)") @@ -368,7 +368,7 @@ def read_data_from_file(file_config: Union[str, Dict], header: bool = True) -> T prop = df[prop_col].to_numpy(dtype=np.float64) else: raise ValueError(f"Property column '{prop_col}' not found in file. " - f"Available columns: {', '.join(df.columns)}") + f"\n -> Available columns: {', '.join(df.columns)}") else: if prop_col >= len(df.columns): raise ValueError(f"Property column index {prop_col} out of bounds (file has {len(df.columns)} columns)") diff --git a/src/pymatlib/core/yaml_parser.py b/src/pymatlib/core/yaml_parser.py index c017473..8d4ff00 100644 --- a/src/pymatlib/core/yaml_parser.py +++ b/src/pymatlib/core/yaml_parser.py @@ -88,7 +88,6 @@ class MaterialConfigParser: """ yaml = YAML(typ='safe') yaml.allow_duplicate_keys = False - try: with open(self.yaml_path, 'r') as f: return yaml.load(f) @@ -116,19 +115,16 @@ class MaterialConfigParser: 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_types(properties) - self._validate_property_values(properties) + # self._validate_property_values(properties) def _validate_property_names(self, properties: Dict[str, Any]) -> None: """ @@ -144,7 +140,6 @@ class MaterialConfigParser: 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 "" @@ -174,6 +169,7 @@ class MaterialConfigParser: ): raise ValueError(f"Invalid configuration for property '{prop_name}': {config}") + #TODO: Deprecated! @staticmethod def _validate_property_values(properties: Dict[str, Any]) -> None: """ @@ -186,38 +182,131 @@ class MaterialConfigParser: for prop_name, prop_value in properties.items(): # print(f"prop_name: {prop_name}, type: {type(prop_name)}") # print(f"prop_value: {prop_value}, type: {type(prop_value)}") - 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"'thermal_expansion_coefficient' value {prop_value} is outside the expected range (-3e-5/K to 0.001/K)") - + 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"'energy_density_temperature_array' must be a tuple of three comma-separated values representing (start, end, points/step)") - + 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") + @staticmethod + def _validate_property_value(prop: str, value: Union[float, np.ndarray]) -> None: + """ + Validate a property value or array of values against physical constraints. + Args: + prop (str): The name of the property being validated + value (Union[float, np.ndarray]): The value or array of values to validate + Raises: + ValueError: If the property value violates physical constraints + TypeError: If the property value has an invalid type + """ + # Define property constraints with SI units + BASE_PROPERTIES = {'base_temperature', 'base_density'} # always single float values + POSITIVE_PROPERTIES = {'density', 'heat_capacity', 'heat_conductivity', 'temperature', + 'dynamic_viscosity', 'kinematic_viscosity', 'thermal_diffusivity', + 'surface_tension'} + NON_NEGATIVE_PROPERTIES = {'latent_heat_of_fusion', 'latent_heat_of_vaporization', 'energy_density', + 'energy_density_solidus', 'energy_density_liquidus', 'energy_density_array'} + ARRAY_PROPERTIES = {'temperature_array', 'energy_density_temperature_array', 'energy_density_array'} + + # Property-specific range constraints + PROPERTY_RANGES = { + 'base_temperature': (0, 5000), # K + 'temperature': (0, 5000), # K + 'base_density': (800, 22000), # kg/m³ + 'density': (800, 22000), # kg/m³ + 'heat_capacity': (100, 10000), # J/(kg·K) + 'heat_conductivity': (1, 600), # W/(m·K) + 'thermal_expansion_coefficient': (-5e-5, 3e-5), # 1/K + 'dynamic_viscosity': (1e-4, 1e5), # Pa·s + 'kinematic_viscosity': (1e-8, 1e-3), # m²/s + 'thermal_diffusivity': (1e-8, 1e-3), # m²/s + 'surface_tension': (0.1, 3.0), # N/m + 'latent_heat_of_fusion': (0, 600000), # J/kg + 'latent_heat_of_vaporization': (50000, 12000000), # J/kg + } + + try: + # Handle arrays (from file or key-val properties) + if isinstance(value, np.ndarray): + # Check for NaN or infinite values + if np.isnan(value).any(): + raise ValueError(f"Property '{prop}' contains NaN values.") + if np.isinf(value).any(): + raise ValueError(f"Property '{prop}' contains infinite values.") + + # Property-specific validations for arrays + if prop in POSITIVE_PROPERTIES: + if (value <= 0).any(): + bad_indices = np.where(value <= 0)[0] + bad_values = value[value <= 0] + raise ValueError(f"All '{prop}' values must be positive. Found {len(bad_indices)} invalid values " + f"at indices {bad_indices}: {bad_values}.") + + if prop in NON_NEGATIVE_PROPERTIES or prop in ARRAY_PROPERTIES: + if (value < 0).any(): + bad_indices = np.where(value < 0)[0] + bad_values = value[value < 0] + raise ValueError(f"All '{prop}' values must be non-negative. Found {len(bad_indices)} invalid values " + f"at indices {bad_indices}: {bad_values}.") + + # Check range constraints if applicable + if prop in PROPERTY_RANGES: + min_val, max_val = PROPERTY_RANGES[prop] + if ((value < min_val) | (value > max_val)).any(): + out_of_range = np.where((value < min_val) | (value > max_val))[0] + out_values = value[out_of_range] + raise ValueError(f"'{prop}' contains values outside expected range ({min_val} to {max_val}) " + f"\n -> Found {len(out_of_range)} out-of-range values at indices {out_of_range}: {out_values}") + + # Handle single values (from constant or computed properties) + else: + # Check for NaN or infinite values + if np.isnan(value): + raise ValueError(f"Property '{prop}' is NaN.") + if np.isinf(value): + raise ValueError(f"Property '{prop}' is infinite.") + + # Type checking + if not isinstance(value, float): + raise TypeError(f"Property '{prop}' must be a float, got {type(value).__name__}. " + f"\n -> Please use decimal notation (e.g., 1.0 instead of 1) or scientific notation.") + + # Property-specific validations for single values + if prop in BASE_PROPERTIES or prop in POSITIVE_PROPERTIES: + if value <= 0: + raise ValueError(f"Property '{prop}' must be positive, got {value}.") + + if prop in NON_NEGATIVE_PROPERTIES: + if value < 0: + raise ValueError(f"Property '{prop}' must be non-negative, got {value}.") + + # Check range constraints if applicable + if prop in PROPERTY_RANGES: + min_val, max_val = PROPERTY_RANGES[prop] + if value < min_val or value > max_val: + raise ValueError(f"Property '{prop}' value {value} is outside expected range ({min_val} to {max_val}).") + + except Exception as e: + raise ValueError(f"Failed to validate property value \n -> {e}") + ################################################## # Alloy Creation ################################################## @@ -244,7 +333,7 @@ class MaterialConfigParser: except KeyError as e: raise ValueError(f"Configuration error: Missing {e}") except Exception as e: - raise ValueError(f"Failed to create alloy: {e}") + raise ValueError(f"Failed to create alloy \n -> {e}") def _get_elements(self) -> List[ChemicalElement]: """ @@ -264,20 +353,25 @@ class MaterialConfigParser: # Property Type Checking ################################################## @staticmethod - def _is_numeric(value: str) -> bool: + def _is_numeric(value: Any) -> bool: """ - Check if string represents a number (including scientific notation). + Check if string represents a float number (including scientific notation). Args: - value (str): The string to check. + value (Any): The value to check. Returns: - bool: True if the string represents a number, False otherwise. + bool: True if the value represents a float, False otherwise. """ - try: - float(value) - print(f"{value}, {type(value)} -> {float(value)}, {type(float(value))}") + if isinstance(value, float): return True - except ValueError: - return False + if isinstance(value, str): + try: + float(value) + # return True + # Ensure it contains a decimal point or is in scientific notation + return '.' in value or 'e' in value.lower() + except ValueError: + return False + return False @staticmethod def _is_data_file(value: str | Dict[str, str]) -> bool: @@ -288,19 +382,23 @@ class MaterialConfigParser: Returns: bool: True if it's a valid data file configuration, False otherwise. Raises: - ValueError: If the file configuration is invalid. + 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 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()) + # Check for missing required keys + missing_keys = required_keys - value_keys if missing_keys: raise ValueError(f"Missing required keys for file configuration: {missing_keys}") + # Check for extra keys + extra_keys = value_keys - required_keys + if extra_keys: + raise ValueError(f"Extra keys found in file configuration: {extra_keys}") return True return False @@ -312,8 +410,23 @@ class MaterialConfigParser: 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. """ - return isinstance(value, dict) and 'key' in value and 'val' in value + required_keys = {'key', 'val'} + # Check if it looks like it's trying to be a key-val property + if isinstance(value, dict) and any(k in value for k in required_keys): + value_keys = set(value.keys()) + # Check for missing required keys + missing_keys = required_keys - value_keys + if missing_keys: + raise ValueError(f"Missing required keys for key-val property: {missing_keys}") + # Check for extra keys + extra_keys = value_keys - required_keys + if extra_keys: + raise ValueError(f"Extra keys found in key-val property: {extra_keys}") + return True + return False @staticmethod def _is_compute_property(value: Any) -> bool: @@ -327,8 +440,12 @@ class MaterialConfigParser: ValueError: If the compute property configuration is invalid. """ # Simple format: property_name: compute - if isinstance(value, str) and value == 'compute': - return True + if isinstance(value, str): + if value == 'compute': + return True + elif value.startswith('compute'): + # Catch common mistakes like 'compute1' + raise ValueError(f"Invalid compute property value: '{value}'. Did you mean 'compute'?") # Advanced format: property_name: { compute: "method_name" } elif isinstance(value, dict) and 'compute' in value: # Ensure no other keys are present @@ -347,18 +464,23 @@ class MaterialConfigParser: 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 + try: + if isinstance(config, int): + raise ValueError(f"Property '{prop_name}' must be defined as a float, got {config} of type {type(config).__name__}") + if 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 + except Exception as e: + raise ValueError(f"Failed to determine property type \n -> {e}") def _categorize_properties(self, properties: Dict[str, Any]) -> Dict[PropertyType, List[Tuple[str, Any]]]: """ @@ -377,13 +499,15 @@ class MaterialConfigParser: 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)) - + try: + prop_type = self._determine_property_type(prop_name, config) + if prop_type == PropertyType.INVALID: + raise ValueError(f"Invalid configuration format for property '{prop_name}': {config}") + categorized_properties[prop_type].append((prop_name, config)) + except Exception as e: + # Provide more context in the error message + raise ValueError(f"Failed to categorize properties \n -> {e}") return categorized_properties ################################################## @@ -400,10 +524,8 @@ class MaterialConfigParser: 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: @@ -419,7 +541,7 @@ class MaterialConfigParser: pass except Exception as e: - raise ValueError(f"Failed to process properties: {e}") + raise ValueError(f"Failed to process properties \n -> {e}") #TODO: Deprecated! def _process_properties1(self, alloy: Alloy, T: Union[float, sp.Symbol]) -> None: @@ -454,21 +576,23 @@ class MaterialConfigParser: ######################################################################################################################## @staticmethod - def _process_constant_property(alloy: Alloy, prop_name: str, prop_config: Union[int, float, str]) -> None: + def _process_constant_property(alloy: Alloy, prop_name: str, prop_config: Union[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. + prop_config (Union[float, str]): The property value or string representation. Raises: - ValueError: If the property value cannot be converted to float. + ValueError: If the property value cannot be converted to float or violates constraints. """ try: value = float(prop_config) + # Validate the value + MaterialConfigParser._validate_property_value(prop_name, value) setattr(alloy, prop_name, value) except (ValueError, TypeError) as e: - error_msg = f"Invalid number format for {prop_name}: {prop_config}" + error_msg = f"Failed to process constant property \n -> {e}" raise ValueError(error_msg) from e ######################################################################################################################## @@ -487,7 +611,6 @@ class MaterialConfigParser: 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: # print("if isinstance(file_config, dict) and 'file' in file_config:") @@ -528,10 +651,11 @@ class MaterialConfigParser: 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)''' - + # Validate the property array + self._validate_property_value(prop_name, prop_array) 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)}" + error_msg = f"Failed to process file property {prop_name} \n -> {str(e)}" raise ValueError(error_msg) from e ######################################################################################################################## @@ -551,13 +675,13 @@ class MaterialConfigParser: print(f"Process key val property: {prop_name}") 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") - + # Validate the value array + self._validate_property_value(prop_name, val_array) 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)}" + error_msg = f"Failed to proces key-val property '{prop_name}' \n -> {str(e)}" raise ValueError(error_msg) from e def _process_key_definition(self, key_def, val_array, alloy: Alloy) -> np.ndarray: @@ -580,7 +704,7 @@ class MaterialConfigParser: else: raise ValueError(f"Invalid key definition: {key_def}") except Exception as e: - error_msg = f"Error processing key definition: {str(e)}" + error_msg = f"Failed to process key definition \n -> {str(e)}" raise ValueError(error_msg) from e @staticmethod @@ -603,7 +727,7 @@ class MaterialConfigParser: 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)}" + error_msg = f"Invalid equidistant format: {key_def} \n -> {str(e)}" raise ValueError(error_msg) from e @staticmethod @@ -633,7 +757,7 @@ class MaterialConfigParser: key_array = np.array(processed_key, dtype=float) return key_array except Exception as e: - error_msg = f"Error processing list key: {str(e)}" + error_msg = f"Error processing list key \n -> {str(e)}" raise ValueError(error_msg) from e ################################################## @@ -660,7 +784,7 @@ class MaterialConfigParser: else: 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)}" + error_msg = f"Error processing property data for '{prop_name}' \n -> {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: @@ -676,10 +800,8 @@ class MaterialConfigParser: # 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) @@ -697,7 +819,6 @@ class MaterialConfigParser: # 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) @@ -731,19 +852,15 @@ class MaterialConfigParser: 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) print(computation_methods) 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'] print(method) @@ -751,18 +868,23 @@ class MaterialConfigParser: 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] print(method_dependencies) - # Process dependencies self._process_dependencies(alloy, prop_name, method_dependencies, T) - # Compute property material_property = computation_methods[prop_name][method]() + # Validate the computed property + '''if isinstance(material_property, (int, float)): + self._validate_property_value(prop_name, material_property) + # For MaterialProperty objects (symbolic) + elif hasattr(material_property, 'evalf') and isinstance(T, sp.Symbol): + # Sample at a few points to validate + if hasattr(alloy, 'temperature_array') and len(alloy.temperature_array) > 0: + sample_values = material_property.evalf(T, alloy.temperature_array) + self._validate_property_value(prop_name, sample_values)''' 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) @@ -809,7 +931,7 @@ class MaterialConfigParser: alloy.density, alloy.specific_enthalpy ), - } + }, } @staticmethod @@ -844,7 +966,6 @@ class MaterialConfigParser: 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. """ @@ -857,7 +978,6 @@ class MaterialConfigParser: if dep_config == 'compute' or (isinstance(dep_config, dict) and 'compute' in dep_config): print("hihihiiiiiiii") self._process_computed_property(alloy, dep, T) - # Verify all dependencies are available missing_deps = [dep for dep in dependencies if getattr(alloy, dep, None) is None] if missing_deps: @@ -876,23 +996,21 @@ class MaterialConfigParser: 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 = np.vectorize(lambda temp: material_property.evalf(T, temp))(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) + 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) ################################################## # Energy Density Temperature Array Processing @@ -917,56 +1035,44 @@ class MaterialConfigParser: """ 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("'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(): return self._process_float_step(start, end, float(step)) else: return self._process_int_step(start, end, int(step)) - except ValueError as e: - raise ValueError(f"Invalid temperature array definition: {e}") + raise ValueError(f"Invalid temperature array definition \n -> {e}") @staticmethod def _process_float_step(start: float, end: float, delta: float) -> np.ndarray: """Process temperature array with float step (delta).""" print(f"Processing EDTA as float: start={start}, end={end}, delta={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).""" print(f"Processing EDTA as int: start={start}, end={end}, points={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) ################################################## -- GitLab