diff --git a/src/pymat/properties.py b/src/pymat/properties.py index c5bb07d..74afc49 100644 --- a/src/pymat/properties.py +++ b/src/pymat/properties.py @@ -157,6 +157,23 @@ class ThermalProperties: min_service_temp_unit: str = "degC" thermal_shock_resistance: Optional[str] = None # "excellent", "good", "fair", "poor" + # Cryogenic radiation-shielding budgets (#152) — polished Al ≈ 0.04, anodized ≈ 0.8 + emissivity: Optional[float] = None # 0–1, dimensionless + # Heat-pulse propagation (#152) + thermal_diffusivity: Optional[float] = None # mm²/s + thermal_diffusivity_unit: str = "mm^2/s" + # Cryogenic embrittlement boundary (#152) — Delrin/PEEK get brittle below ~-50 °C + min_use_temp_K: Optional[float] = None + cryogenic_compatible: Optional[bool] = None + # NIST-style ∫k dT for cryogenic heat-leak (#152) + integrated_thermal_conductivity: Optional[float] = None # W/m + integrated_thermal_conductivity_unit: str = "W/m" + # Phase-change enthalpies for liquids/coolants (#152) + latent_heat_fusion: Optional[float] = None # kJ/kg + latent_heat_fusion_unit: str = "kJ/kg" + latent_heat_vaporization: Optional[float] = None # kJ/kg + latent_heat_vaporization_unit: str = "kJ/kg" + # T-dependent curves (#148). Sibling fields parallel to scalars. thermal_conductivity_curve: Optional[TempCurve] = None specific_heat_curve: Optional[TempCurve] = None @@ -223,6 +240,32 @@ def min_service_temp_qty(self) -> Optional[Quantity]: return (self.min_service_temp + 273.15) * ureg.kelvin return self.min_service_temp * ureg(self.min_service_temp_unit) + @property + def thermal_diffusivity_qty(self) -> Optional[Quantity]: + if self.thermal_diffusivity is None: + return None + return self.thermal_diffusivity * ureg(self.thermal_diffusivity_unit) + + @property + def integrated_thermal_conductivity_qty(self) -> Optional[Quantity]: + if self.integrated_thermal_conductivity is None: + return None + return self.integrated_thermal_conductivity * ureg( + self.integrated_thermal_conductivity_unit + ) + + @property + def latent_heat_fusion_qty(self) -> Optional[Quantity]: + if self.latent_heat_fusion is None: + return None + return self.latent_heat_fusion * ureg(self.latent_heat_fusion_unit) + + @property + def latent_heat_vaporization_qty(self) -> Optional[Quantity]: + if self.latent_heat_vaporization is None: + return None + return self.latent_heat_vaporization * ureg(self.latent_heat_vaporization_unit) + def thermal_conductivity_at(self, temp: Quantity) -> Optional[Quantity]: """ Calculate thermal conductivity at given temperature. diff --git a/tests/test_thermal_fields.py b/tests/test_thermal_fields.py new file mode 100644 index 0000000..e4e2c35 --- /dev/null +++ b/tests/test_thermal_fields.py @@ -0,0 +1,130 @@ +"""Tests for #152 — thermal sub-table additions. + +Pure additive: 7 new optional fields on `ThermalProperties` for +cryogenic + coolant work. +""" + +from __future__ import annotations + +from textwrap import dedent + +from pymat.loader import load_toml + + +class TestNewThermalFields: + def test_emissivity_loads(self, tmp_path): + """Critical for cryogenic radiation shielding budgets.""" + p = tmp_path / "m.toml" + p.write_text( + dedent(""" + [polished_al] + name = "Polished Al" + [polished_al.thermal] + emissivity = 0.04 + """) + ) + m = load_toml(p)["polished_al"] + assert m.properties.thermal.emissivity == 0.04 + + def test_thermal_diffusivity_loads(self, tmp_path): + p = tmp_path / "m.toml" + p.write_text( + dedent(""" + [m] + name = "M" + [m.thermal] + thermal_diffusivity_value = 97 + thermal_diffusivity_unit = "mm^2/s" + """) + ) + m = load_toml(p)["m"] + assert m.properties.thermal.thermal_diffusivity == 97 + + def test_min_use_temp_K_and_cryogenic_compat(self, tmp_path): + """Delrin/PEEK get brittle below ~-50 °C; track that explicitly.""" + p = tmp_path / "m.toml" + p.write_text( + dedent(""" + [delrin] + name = "Delrin" + [delrin.thermal] + min_use_temp_K = 223 + cryogenic_compatible = false + + [peek] + name = "PEEK" + [peek.thermal] + min_use_temp_K = 4 + cryogenic_compatible = true + """) + ) + mats = load_toml(p) + assert mats["delrin"].properties.thermal.min_use_temp_K == 223 + assert mats["delrin"].properties.thermal.cryogenic_compatible is False + assert mats["peek"].properties.thermal.cryogenic_compatible is True + + def test_integrated_thermal_conductivity_loads(self, tmp_path): + """NIST-style ∫k dT for cryogenic heat-leak calculations.""" + p = tmp_path / "m.toml" + p.write_text( + dedent(""" + [m] + name = "M" + [m.thermal] + integrated_thermal_conductivity_value = 1430 + integrated_thermal_conductivity_unit = "W/m" + """) + ) + m = load_toml(p)["m"] + assert m.properties.thermal.integrated_thermal_conductivity == 1430 + + def test_latent_heats_load(self, tmp_path): + p = tmp_path / "m.toml" + p.write_text( + dedent(""" + [ln2] + name = "Liquid N2" + [ln2.thermal] + latent_heat_fusion_value = 25.7 + latent_heat_fusion_unit = "kJ/kg" + latent_heat_vaporization_value = 199 + latent_heat_vaporization_unit = "kJ/kg" + """) + ) + m = load_toml(p)["ln2"] + assert m.properties.thermal.latent_heat_fusion == 25.7 + assert m.properties.thermal.latent_heat_vaporization == 199 + + def test_pint_qty_for_new_dimensional_fields(self, tmp_path): + p = tmp_path / "m.toml" + p.write_text( + dedent(""" + [m] + name = "M" + [m.thermal] + thermal_diffusivity_value = 97 + thermal_diffusivity_unit = "mm^2/s" + integrated_thermal_conductivity_value = 1430 + integrated_thermal_conductivity_unit = "W/m" + latent_heat_fusion_value = 25.7 + latent_heat_fusion_unit = "kJ/kg" + latent_heat_vaporization_value = 199 + latent_heat_vaporization_unit = "kJ/kg" + """) + ) + m = load_toml(p)["m"] + t = m.properties.thermal + assert t.thermal_diffusivity_qty.to("mm^2/s").magnitude == 97 + assert t.integrated_thermal_conductivity_qty.to("W/m").magnitude == 1430 + assert t.latent_heat_fusion_qty.to("kJ/kg").magnitude == 25.7 + assert t.latent_heat_vaporization_qty.to("kJ/kg").magnitude == 199 + + +class TestRegression: + def test_full_corpus_loads(self): + from pymat import _CATEGORY_BASES + from pymat.loader import load_category + + for cat in _CATEGORY_BASES: + mats = load_category(cat) + assert mats, f"category {cat} loaded empty"