Source code for upxo.material.Material

"""
Material.py
============
Aggregates a material's data across many categories -- identity,
processing route, crystal structure, texture, mechanical/physical
properties, irradiation condition, EBSD-derived statistics, etc. -- into
one ``MaterialRegistry`` (see ``registry.py``), replacing the old
``ObjectDataDictionary``-based ``build()``, which flattened every category
to a plain dict on write (discarding type information immediately) and
offered no validation or extensibility beyond editing this function
directly every time a category was added.

See ``admin/material/scoping.md`` and
``admin/material/implementation_plan.md`` for the full redesign rationale.
Categories that used to live directly in this file --
``MaterialIdentity``, ``ProcessingCondition``, ``TexCompVolFracFCC``,
``TexFibreVolFracFCC``, ``TexCompWidth`` -- have moved to their own files
(``identity.py``, ``processing.py``, ``texture.py``) or been superseded
outright (``ProcessingCondition`` -> ``ProcessingRoute``; the three
``Tex*`` classes -> ``TextureComponentProfile``). Everything below is
carried over unchanged in content -- only the container it's registered
into has changed.

There is no ``CrossCheckAndAppend`` equivalent here (the old, broken,
never-exercised update-in-place path -- see
``admin/material/scoping.md`` §2, item 5): calling
``MaterialRegistry.ingest()`` again for an already-registered category
simply replaces its stored instance and provenance, which covers the same
"update on a segment-by-segment basis" use case without a second method.
"""

from dataclasses import dataclass, field
import numpy as np

# Soft-validation whitelist for CrystalFamily.xtal_family -- the values the
# texture work (admin/twinnedFccGui/texIntegration/) actually cares about.
# Deliberately does NOT include this class's own pre-existing default
# ('mmm', a crystallographic point-group symbol, not a crystal-family
# name -- a different classification system entirely): build()'s default
# CrystalFamily() correctly triggers one soft-validation warning as a
# result, same as MaterialIdentity()'s default 'cu' does against
# KNOWN_MATERIALS below -- both are informal legacy placeholders, not real
# entries in their respective curated sets, and flagging that is soft
# validation doing exactly its job, not a bug to special-case away.
KNOWN_CRYSTAL_FAMILIES = {'FCC', 'BCC', 'HCP'}


[docs] def build(): """ Build and return the material data base using classes in this module. When no inputs are provided, defaults prescribed in the class data are used. Returns ------- MaterialRegistry Typed, extensible, provenance-tracking material data container (see ``upxo.material.registry``). Access a category's instance via ``matdata.get("CategoryName")``, and its provenance via ``matdata.get_provenance("CategoryName")``. Examples -------- >>> from upxo.material.Material import build >>> matdata = build() >>> matdata.categories() """ from upxo.material.registry import MaterialRegistry from upxo.material.identity import MaterialIdentity, KNOWN_MATERIALS from upxo.material.processing import ProcessingRoute from upxo.material.texture import TextureComponentProfile matdata = MaterialRegistry() matdata.register_category("MaterialIdentity", MaterialIdentity, known_values={"name": KNOWN_MATERIALS}) matdata.register_category("ProcessingRoute", ProcessingRoute) matdata.register_category("IrradiationCondition", IrradiationCondition) matdata.register_category("CrystalFamily", CrystalFamily, known_values={"xtal_family": KNOWN_CRYSTAL_FAMILIES}) matdata.register_category("Phases", Phases) matdata.register_category("PhysicalProperty", PhysicalProperty) matdata.register_category("ElasticProperty", ElasticProperty) matdata.register_category("TensileStressStrain", TensileStressStrain) matdata.register_category("PlasticProperty", PlasticProperty) matdata.register_category("ExpDataAvailability", ExpDataAvailability) matdata.register_category("GrainEqDiaEbsd", GrainEqDiaEbsd) matdata.register_category("TextureComponentProfile", TextureComponentProfile) matdata.register_category("EBSDParameters", EBSDParameters) matdata.register_category("TensileTestParameters", TensileTestParameters) default_source = "Material.py build() default" matdata.ingest("MaterialIdentity", MaterialIdentity(), source=default_source) matdata.ingest("ProcessingRoute", ProcessingRoute(), source=default_source) matdata.ingest("IrradiationCondition", IrradiationCondition(), source=default_source) matdata.ingest("CrystalFamily", CrystalFamily(), source=default_source) matdata.ingest("Phases", Phases(), source=default_source) matdata.ingest("PhysicalProperty", PhysicalProperty(), source=default_source) matdata.ingest("ElasticProperty", ElasticProperty(), source=default_source) matdata.ingest("TensileStressStrain", TensileStressStrain(), source=default_source) matdata.ingest("PlasticProperty", PlasticProperty(), source=default_source) matdata.ingest("ExpDataAvailability", ExpDataAvailability(), source=default_source) matdata.ingest("GrainEqDiaEbsd", GrainEqDiaEbsd(), source=default_source) matdata.ingest("TextureComponentProfile", TextureComponentProfile(crystal_family="FCC"), source=default_source) matdata.ingest("EBSDParameters", EBSDParameters(), source=default_source) matdata.ingest("TensileTestParameters", TensileTestParameters(), source=default_source) return matdata
[docs] @dataclass(frozen = True, repr = True) class IrradiationCondition: """Irradiation environment (type, temperature K, dose in dpa).""" irr : str = field(default = 'neutron') # Type of irradiation irr_temp: float = field(default = 400) # Temperature of irradiation in Kelvin irr_dpa : float = field(default = 1E-5) # displacements per atom
[docs] @dataclass(frozen = True, repr = True) class CrystalFamily: """Crystal family / system label for texture and phase work. Soft-validated against ``KNOWN_CRYSTAL_FAMILIES`` (``FCC``/``BCC``/``HCP``) when registered via :func:`build`. Default ``'mmm'`` is a legacy placeholder and intentionally triggers a soft warning. """ xtal_family: str = field(default = 'mmm') # Crystal family: mmm, etc
[docs] @dataclass(frozen = True, repr = True) class Phases: """Phase count, names, and volume/area fractions.""" nphases: int = field(default = 2) # Number of phases namesPhases: np.ndarray = field(default_factory=lambda: np.ndarray([], dtype=str)) phaseFractions: np.ndarray = field(default_factory=lambda: np.ndarray([], dtype=float))
[docs] @dataclass(frozen = True, repr = True) class PhysicalProperty: """Bulk physical properties (density in kg/m^3 by default).""" density: float = field(default = 2700.0) # in kg m^-3
[docs] @dataclass(frozen = True, repr = True) class ElasticProperty: """Elastic constants (Young's modulus ``E`` in MPa by default).""" E: float = field(default = 70E3) # Young's modulus in MPa
[docs] @dataclass(frozen = True, repr = True) class TensileStressStrain: """Experimental tensile stress–strain curve arrays.""" strain: np.ndarray = field(default_factory=lambda: np.array([], dtype=float)) stress: np.ndarray = field(default_factory=lambda: np.array([], dtype=float))
[docs] @dataclass(frozen = True, repr = True) class PlasticProperty: """Proof strengths, hardness numbers, and fracture toughness.""" Sy001: float = field(default = 135) # 0.1% proof strength, MPa Sy002: float = field(default = 150) # 0.2% proof strength, MPa Sy003: float = field(default = 155) # 0.3% proof strength, MPa HV0005: float = field(default = 50) # Vicker's hardness number @ 0.005 kg-f HV0010: float = field(default = 50) # Vicker's hardness number @ 0.010 kg-f HV0020: float = field(default = 50) # Vicker's hardness number @ 0.020 kg-f K: float = field(default = 1234) # Fracture toughness
[docs] @dataclass(frozen = True, repr = True) class ExpDataAvailability: """Flags for which experimental datasets exist for this material.""" tt : bool = field(default = True) # tensile test. True if available else False fatigue_low : bool = field(default = True) fatigue_high: bool = field(default = True) ebsd : bool = field(default = True) tem : bool = field(default = True)
[docs] @dataclass(frozen = True, repr = True) class GrainEqDiaEbsd: """EBSD-derived equivalent-diameter distribution summary and histograms.""" modality: int = field(default = 2) skewness: float = field(default = -1.02) kurtosis: float = field(default = 1.24) dist_grain_size : np.ndarray = field(default_factory=lambda: np.array([], dtype=float)) dist_grain_count: np.ndarray = field(default_factory=lambda: np.array([], dtype=int)) dist_grain_prob : np.ndarray = field(default_factory=lambda: np.array([], dtype=float))
[docs] @dataclass(frozen = True, repr = True) class EBSDParameters: """EBSD map quality metrics (zero fractions, phase fractions).""" zero_fraction_uncorrected : float = field(default = 0.0)# 0 to 1 zero_fraction_corrected : float = field(default = 0.0)# 0 to 1 phase_fraction: np.ndarray = field(default_factory=lambda: np.array([], dtype=float))# data for every phase
[docs] @dataclass(frozen = True, repr = True) class TensileTestParameters: """Tensile-test setup (sample geometry, rate, temperature).""" sample_type : str = field(default = 'value')# 'dogbonegrip','dogboneshoulder' strain_rate : float = field(default = 0.0) test_temperature: float = field(default = 0.0)
#______________________________________________________________________________ # HELPER METHODS:
[docs] def TempKelvin(temp_celcius): """Tempkelvin.""" # This is only to demonstrate a way of setting value in a dataclass. # Value could infact be taken directly in Kelvin, inside the "IrradiationCondition" class return 273.0 + temp_celcius
#______________________________________________________________________________ # CALLER METHODS
[docs] def generate(): ''' ''' return build()
#______________________________________________________________________________