Source code for materforge.parsing.api

"""
Main API module for MaterForge material property library.

This module provides the primary interface for creating and working with material objects
from YAML configuration files. It includes functions for material creation, validation,
property evaluation, and information extraction.
"""

import logging
from pathlib import Path
from typing import Dict, List, Optional, Union

import sympy as sp

from materforge.core.materials import Material
from materforge.parsing.config.material_yaml_parser import MaterialYAMLParser
from materforge.parsing.config.yaml_keys import (
    NAME_KEY, MATERIAL_TYPE_KEY, COMPOSITION_KEY, PROPERTIES_KEY,
    PURE_METAL_KEY, MELTING_TEMPERATURE_KEY, BOILING_TEMPERATURE_KEY,
    SOLIDUS_TEMPERATURE_KEY, LIQUIDUS_TEMPERATURE_KEY,
    INITIAL_BOILING_TEMPERATURE_KEY, FINAL_BOILING_TEMPERATURE_KEY,
    ALLOY_KEY
)

logger = logging.getLogger(__name__)


# ====================================================================
# CORE MATERIAL CREATION AND VALIDATION
# ====================================================================

[docs] def create_material(yaml_path: Union[str, Path], dependency: sp.Symbol, enable_plotting: bool = True) -> Material: """Create material instance from YAML configuration file. This function serves as the main entry point for creating material objects from YAML configuration files. It handles the parsing of the configuration and creation of the material with the specified temperature. Parameters ---------- yaml_path : Union[str, Path] Path to the YAML configuration file dependency : sp.Symbol Sympy symbol for property evaluation. Use a symbolic variable (e.g., sp.Symbol('T') or sp.Symbol('u_C')) for symbolic temperature expressions enable_plotting : bool, optional Whether to generate visualization plots (default: True) Returns ------- Material The material instance with all properties initialized Raises ------ FileNotFoundError If the YAML file doesn't exist ValueError If the YAML content is invalid or material creation fails TypeError If temperature parameter has invalid type Notes ----- In YAML files, always use 'T' as the temperature variable in equations. The system will automatically substitute this with your provided symbol. Examples -------- Create a material with symbolic temperature expressions: >>> import sympy as sp >>> T = sp.Symbol('T') >>> material = create_material('steel.yaml', T) >>> print(material.name) Steel Create a material with a custom temperature symbol: >>> u_C = sp.Symbol('u_C') >>> material_copper = create_material('copper.yaml', u_C) """ logger.info("Creating material from: %s with dependency=%s, plotting=%s", yaml_path, dependency, enable_plotting) try: # Accept symbolic temperatures only if not isinstance(dependency, sp.Symbol): raise TypeError(f"Dependency '{dependency}' must be a sympy Symbol, got {type(dependency)}") parser = MaterialYAMLParser(yaml_path=yaml_path) material = parser.create_material(dependency=dependency, enable_plotting=enable_plotting) logger.info("Successfully created material: %s with %d properties", material.name, len([attr for attr in dir(material) if not attr.startswith('_') and hasattr(material, attr)])) return material except Exception as e: logger.error("Failed to create material from %s: %s", yaml_path, e, exc_info=True) raise
[docs] def validate_yaml_file(yaml_path: Union[str, Path]) -> bool: """ Validate a YAML file without creating the material. Args: yaml_path: Path to the YAML configuration file to validate Returns: bool: True if the file is valid Raises: FileNotFoundError: If the file doesn't exist ValueError: If the YAML content is invalid Example: try: is_valid = validate_yaml_file('steel.yaml') print(f"YAML file is valid: {is_valid}") except ValueError as e: print(f"Validation failed: {e}") """ logger.info("Validating YAML file: %s", yaml_path) try: _ = MaterialYAMLParser(yaml_path) logger.info("YAML validation successful for: %s", yaml_path) return True except FileNotFoundError as e: logger.error("YAML file not found: %s", yaml_path) raise FileNotFoundError(f"YAML file not found: {yaml_path}") from e except ValueError as e: logger.error("YAML validation failed for %s: %s", yaml_path, e) raise ValueError(f"YAML validation failed: {str(e)}") from e except Exception as e: logger.error("Unexpected error validating YAML %s: %s", yaml_path, e, exc_info=True) raise ValueError(f"Unexpected error validating YAML: {str(e)}") from e
# ==================================================================== # MATERIAL INFORMATION AND PROPERTIES # ====================================================================
[docs] def get_material_info(yaml_path: Union[str, Path]) -> Dict: """ Get basic information about a material configuration without full processing. Args: yaml_path: Path to the YAML configuration file Returns: Dict: Dictionary containing material information including: - name: Material name - material_type: Type of material (pure_metal or alloy) - composition: Element composition dictionary - properties: List of available property names - total_properties: Number of properties defined - property_types: Count of each property type - Temperature properties based on material type Raises: FileNotFoundError: If the YAML file doesn't exist ValueError: If the YAML content is invalid Example: info = get_material_info('steel.yaml') print(f"Material: {info['name']}") print(f"Properties: {info['total_properties']}") print(f"Type: {info['material_type']}") """ logger.info("Extracting material info from: %s", yaml_path) try: parser = MaterialYAMLParser(yaml_path=yaml_path) config = parser.config # Base information info = { 'name': config.get(NAME_KEY, 'Unknown'), 'material_type': config.get(MATERIAL_TYPE_KEY, 'Unknown'), 'composition': config.get(COMPOSITION_KEY, {}), } # Add properties information properties = config.get(PROPERTIES_KEY, {}) info['properties'] = list(properties.keys()) info['total_properties'] = len(properties) # Add temperature-specific properties based on material type if info['material_type'] == PURE_METAL_KEY: info['melting_temperature'] = config.get(MELTING_TEMPERATURE_KEY, 'Undefined') info['boiling_temperature'] = config.get(BOILING_TEMPERATURE_KEY, 'Undefined') elif info['material_type'] == ALLOY_KEY: info['solidus_temperature'] = config.get(SOLIDUS_TEMPERATURE_KEY, 'Undefined') info['liquidus_temperature'] = config.get(LIQUIDUS_TEMPERATURE_KEY, 'Undefined') info['initial_boiling_temperature'] = config.get(INITIAL_BOILING_TEMPERATURE_KEY, 'Undefined') info['final_boiling_temperature'] = config.get(FINAL_BOILING_TEMPERATURE_KEY, 'Undefined') # Add property categorization info if available if hasattr(parser, 'categorized_properties') and parser.categorized_properties: info['property_types'] = { prop_type.name: len(props) for prop_type, props in parser.categorized_properties.items() if len(props) > 0 } logger.info("Successfully extracted info for material: %s", info['name']) return info except FileNotFoundError as e: logger.error("YAML file not found: %s", yaml_path) raise FileNotFoundError(f"YAML file not found: {yaml_path}") from e except KeyError as e: logger.error("Missing required field in YAML %s: %s", yaml_path, e) raise ValueError(f"Missing required field in YAML: {str(e)}") from e except Exception as e: logger.error("Failed to extract material info from %s: %s", yaml_path, e, exc_info=True) raise ValueError(f"Failed to extract material info: {str(e)}") from e
[docs] def get_supported_properties() -> List[str]: """Get a list of all supported material properties. Returns ------- List[str] List of strings representing valid property names that can be defined in YAML files Examples -------- >>> props = get_supported_properties() >>> print(f"Supported properties: {len(props)}") Supported properties: 12 >>> for prop in props[:3]: ... print(f" - {prop}") - density - heat_capacity - thermal_conductivity """ return sorted(list(MaterialYAMLParser.VALID_YAML_PROPERTIES))
[docs] def get_material_property_names(material: Material) -> List[str]: """ Get list of all available property names for a material instance. Args: material: Material instance Returns: List[str]: List of property names that exist (are not None) on the material Raises: ValueError: If material is not a Material instance Example: material = create_material('steel.yaml', dependency=sp.Symbol('T')) available = get_material_property_names(material) print(f"Available properties: {available}") """ if not isinstance(material, Material): raise ValueError(f"Expected Material instance, got {type(material).__name__}") # Define all possible property names property_names = [ 'density', 'dynamic_viscosity', 'energy_density', 'heat_capacity', 'heat_conductivity', 'kinematic_viscosity', 'latent_heat_of_fusion', 'latent_heat_of_vaporization', 'specific_enthalpy', 'surface_tension', 'thermal_diffusivity', 'thermal_expansion_coefficient' ] # Return only properties that exist and are not None return [name for name in property_names if getattr(material, name, None) is not None]
# ==================================================================== # PROPERTY EVALUATION # ====================================================================
[docs] def evaluate_material_properties(material: Material, temperature: Union[float, int], properties: Optional[List[str]] = None, include_constants: bool = True) -> Dict[str, float]: """ Convenience function to evaluate material properties at a specific temperature. This is a wrapper around Material.evaluate_properties_at_temperature() for functional-style usage. Args: material: Material instance temperature: Temperature value in Kelvin properties: List of specific property names to evaluate. If None, evaluates all. include_constants: Whether to include constant properties in the result Returns: Dict[str, float]: Dictionary mapping property names to their evaluated values Raises: ValueError: If material is not a Material instance or temperature is invalid Examples: # Evaluate all properties values = evaluate_material_properties(material, 500.0) # Evaluate specific properties values = evaluate_material_properties(material, 500.0, ['density', 'heat_capacity']) # Get only temperature-dependent properties values = evaluate_material_properties(material, 500.0, include_constants=False) """ logger.info("Evaluating material properties via API function") if not isinstance(material, Material): raise ValueError(f"Expected Material instance, got {type(material).__name__}") return material.evaluate_properties_at_temperature( temperature=temperature, properties=properties, include_constants=include_constants )
# ==================================================================== # INTERNAL/TESTING FUNCTIONS # ==================================================================== def _test_api(): """ Internal test function for API validation. This function is used for internal testing and should not be called by end users. """ try: # Test basic validation test_path = Path("example.yaml") if test_path.exists(): assert validate_yaml_file(test_path) is True logger.info("API test passed") else: logger.warning("Test file not found, skipping API test") except (FileNotFoundError, ValueError, AssertionError) as e: logger.error(f"API test failed: {e}") # ==================================================================== # MODULE EXPORTS # ==================================================================== __all__ = [ 'create_material', 'validate_yaml_file', 'get_material_info', 'get_supported_properties', 'get_material_property_names', 'evaluate_material_properties' ]