diff --git a/src/pymatlib/core/alloy.py b/src/pymatlib/core/alloy.py index 872f052b425cb4d6889946d909ce011d32a0ba86..b7cf7fab72609d756bbd2c6686b8c5ee6457cc64 100644 --- a/src/pymatlib/core/alloy.py +++ b/src/pymatlib/core/alloy.py @@ -5,10 +5,51 @@ from pymatlib.core.elements import (ChemicalElement, interpolate_atomic_mass, interpolate_atomic_number, interpolate_temperature_boil) -from pymatlib.data.element_data import Ti, Al, V from pymatlib.core.typedefs import ArrayTypes, PropertyTypes +class AlloyCompositionError(ValueError): + """Exception raised when alloy composition validation fails.""" + pass + +class AlloyTemperatureError(ValueError): + """Exception raised when alloy temperature validation fails.""" + pass + +'''class PropertyTypeChecker: + """ + A descriptor class for handling property type checking. + + Args: + name (str): Name of the property to check. + + Raises: + TypeError: If the value being set is not None and not of type PropertyTypes. + """ + def __init__(self, name: str): + self.private_name = f'_{name}' + + def __get__(self, obj, objtype=None): + """Get the property value.""" + if obj is None: + return self + return getattr(obj, self.private_name) + + def __set__(self, obj, value): + """ + Set the property value with type checking. + + Args: + obj: The object instance. + value: The value to set. + + Raises: + TypeError: If value is not None and not of type PropertyTypes. + """ + if value is not None and not isinstance(value, get_args(PropertyTypes)): + raise TypeError(f"{self.private_name[1:]} must be of type PropertyTypes (float or MaterialProperty)") + setattr(obj, self.private_name, value)''' + @dataclass class Alloy: """ @@ -34,6 +75,11 @@ class Alloy: surface_tension (PropertyTypes): Surface tension of the alloy. thermal_diffusivity (PropertyTypes): Thermal diffusivity of the alloy. thermal_expansion_coefficient (PropertyTypes): Thermal expansion coefficient of the alloy. + + Raises: + AlloyCompositionError: If the composition fractions don't sum to 1.0. + AlloyTemperatureError: If solidus temperature is greater than liquidus temperature. + ValueError: If elements list is empty or composition length doesn't match elements length. """ elements: List[ChemicalElement] composition: ArrayTypes # List of fractions summing to 1.0 @@ -57,6 +103,29 @@ class Alloy: thermal_diffusivity: PropertyTypes = None thermal_expansion_coefficient: PropertyTypes = None + '''# Private fields for properties + _density: PropertyTypes = field(default=None, init=False, repr=False) + _dynamic_viscosity: PropertyTypes = field(default=None, init=False, repr=False) + _heat_capacity: PropertyTypes = field(default=None, init=False, repr=False) + _heat_conductivity: PropertyTypes = field(default=None, init=False, repr=False) + _kinematic_viscosity: PropertyTypes = field(default=None, init=False, repr=False) + _latent_heat_of_fusion: PropertyTypes = field(default=None, init=False, repr=False) + _latent_heat_of_vaporization: PropertyTypes = field(default=None, init=False, repr=False) + _surface_tension: PropertyTypes = field(default=None, init=False, repr=False) + _thermal_diffusivity: PropertyTypes = field(default=None, init=False, repr=False) + _thermal_expansion_coefficient: PropertyTypes = field(default=None, init=False, repr=False) + + # Property descriptors + density = PropertyTypeChecker("density") + dynamic_viscosity = PropertyTypeChecker("dynamic_viscosity") + heat_capacity = PropertyTypeChecker("heat_capacity") + heat_conductivity = PropertyTypeChecker("heat_conductivity") + kinematic_viscosity = PropertyTypeChecker("kinematic_viscosity") + latent_heat_of_fusion = PropertyTypeChecker("latent_heat_of_fusion") + latent_heat_of_vaporization = PropertyTypeChecker("latent_heat_of_vaporization") + surface_tension = PropertyTypeChecker("surface_tension") + thermal_diffusivity = PropertyTypeChecker("thermal_diffusivity") + thermal_expansion_coefficient = PropertyTypeChecker("thermal_expansion_coefficient")''' def solidification_interval(self) -> Tuple[float, float]: """ @@ -67,67 +136,44 @@ class Alloy: """ return self.temperature_solidus, self.temperature_liquidus - def __post_init__(self) -> None: + def _validate_composition(self) -> None: """ - Initializes properties based on elemental composition and validates the composition and phase transition temperatures. + Validate the alloy composition. Raises: - ValueError: If the sum of the composition array does not equal 1 or if the solidus temperature is greater than the liquidus temperature. + ValueError: If elements list is empty or composition length doesn't match elements length. + AlloyCompositionError: If composition fractions don't sum to 1.0. """ - if not np.isclose(sum(self.composition), 1.0, atol=1e-12): - raise ValueError(f"The sum of the composition array must be 1.0, got {sum(self.composition)}") + if not self.elements: + raise ValueError("Elements list cannot be empty") + if len(self.elements) != len(self.composition): + raise ValueError(f"Number of elements ({len(self.elements)}) must match composition length ({len(self.composition)})") + if not np.isclose(sum(self.composition), 1.0, atol=1e-10): + raise AlloyCompositionError(f"The sum of the composition array must be 1.0, got {sum(self.composition)}") + + def _validate_temperatures(self) -> None: + """ + Validate the alloy temperatures. + Raises: + AlloyTemperatureError: If solidus temperature is greater than liquidus temperature. + """ if self.temperature_solidus > self.temperature_liquidus: - raise ValueError("The solidus temperature must be less than or equal to the liquidus temperature.") + raise AlloyTemperatureError("The solidus temperature must be less than or equal to the liquidus temperature.") - # utils = ElementUtils() + def _calculate_properties(self) -> None: + """Calculate derived properties based on composition.""" self.atomic_number = interpolate_atomic_number(self.elements, self.composition) self.atomic_mass = interpolate_atomic_mass(self.elements, self.composition) self.temperature_boil = interpolate_temperature_boil(self.elements, self.composition) + def __post_init__(self) -> None: + """ + Initialize and validate the alloy properties. -if __name__ == '__main__': - try: - # Valid case - Ti64 = Alloy([Ti, Al, V], [0.90, 0.06, 0.04], 1878, 1928) - print(f"Calculated Atomic Number: {0.90 * Ti.atomic_number + 0.06 * Al.atomic_number + 0.04 * V.atomic_number}") - print(f"Alloy Atomic Number: {Ti64.atomic_number}") - print(f"Initial Heat Conductivity: {Ti64.heat_conductivity}") - Ti64.heat_conductivity = 34 - print(f"Updated Heat Conductivity: {Ti64.heat_conductivity}") - print(f"Boiling Temperature (Before Change): {Ti64.temperature_boil}") - Ti64.temperature_boil = 1000 - print(f"Boiling Temperature (After Change): {Ti64.temperature_boil}") - - # Invalid Composition - try: - invalid_alloy = Alloy([Ti, Al], [0.5, 0.5], 1878, 1928) - except ValueError as e: - print(f"Invalid Composition Test Passed: {e}") - - # Empty Composition - try: - empty_composition_alloy = Alloy([Ti], [], 1878, 1928) - except ValueError as e: - print(f"Empty Composition Test Passed: {e}") - - # Single Element Alloy - single_element_alloy = Alloy([Ti], [1.0], 1878, 1928) - print(f"Single Element Alloy Atomic Number: {single_element_alloy.atomic_number}") - - # Invalid Property Assignment - try: - Ti64.heat_conductivity = "invalid_value" # type: ignore - except TypeError as e: - print(f"Invalid Property Assignment Test Passed: {e}") - - # Boundary Values for Temperatures - boundary_alloy = Alloy([Ti, Al, V], [0.33, 0.33, 0.34], -273.15, 10000) - print(f"Boundary Temperatures: Solidus={boundary_alloy.temperature_solidus}, Liquidus={boundary_alloy.temperature_liquidus}") - - # Properties Initialization - default_alloy = Alloy([Ti], [1.0], 1878, 1928) - print(f"Default Density: {default_alloy.density}") - - except Exception as e: - print(f"An unexpected error occurred: {e}") + Called automatically after the dataclass initialization. + Validates composition and temperatures, then calculates derived properties. + """ + self._validate_composition() + self._validate_temperatures() + self._calculate_properties() \ No newline at end of file