Source code for exlink.materials

"""Material data and fatigue-strength corrections for component sizing.

Unit system
-----------
The structural code uses the consistent set **mm, N, MPa, tonne, s**, so that
``F = m a`` needs no conversion factor: one tonne accelerated at one mm/s^2 is
one newton.  Steel density is therefore ``7.85e-9 t/mm^3``.  Masses are exposed
in kilograms at the API surface, where a reader expects them.

Fatigue model
-------------
Rotating-bending endurance limit corrected by Marin factors, then a Goodman
line, which is the standard textbook treatment (Shigley) and the right level of
fidelity for a first sizing iteration:

.. math::
    \\frac{\\sigma_a}{S_e} + \\frac{\\sigma_m}{S_u} \\le \\frac{1}{n_f}

with ``S_e = k_a k_b k_c k_d k_e S_e'`` and ``S_e' = 0.5 S_u``.  The size factor
``k_b`` depends on the diameter being solved for, which is one reason the sizing
of each member is a small root-find rather than a closed form.
"""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np
from numpy.typing import NDArray

FloatArray = NDArray[np.float64]

STEEL_DENSITY = 7.85e-9
"""Density of steel in the consistent unit system [tonne/mm^3]."""


[docs] @dataclass(frozen=True) class Material: """An isotropic metal, described well enough to size a part against it.""" name: str = "42CrMo4 quenched and tempered" density: float = STEEL_DENSITY """[tonne/mm^3]""" youngs_modulus: float = 210_000.0 """``E`` [MPa].""" yield_strength: float = 700.0 """``S_y`` [MPa].""" ultimate_strength: float = 900.0 """``S_u`` [MPa].""" surface_factor_a: float = 4.51 surface_factor_b: float = -0.265 """``k_a = a S_u^b`` for a machined surface (Shigley, MPa units).""" reliability_factor: float = 0.814 """``k_e`` for 99 % reliability.""" temperature_factor: float = 1.0 """``k_d``; 1.0 at ambient.""" @property def density_kg_per_mm3(self) -> float: """Density in kg/mm^3, for reporting masses in kilograms.""" return self.density * 1000.0 @property def endurance_limit_uncorrected(self) -> float: """``S_e' = 0.5 S_u`` [MPa], capped at 700 MPa as usual for steels.""" return min(0.5 * self.ultimate_strength, 700.0) @property def surface_factor(self) -> float: """``k_a``, the machined-surface correction.""" return self.surface_factor_a * self.ultimate_strength**self.surface_factor_b
[docs] def size_factor(self, diameter: FloatArray | float) -> FloatArray: """``k_b``, the size correction for a solid round section. Args: diameter: Section diameter [mm]. Returns: ``k_b``, elementwise. """ d = np.atleast_1d(np.asarray(diameter, dtype=float)) factor = np.ones_like(d) small = (d >= 2.79) & (d <= 51.0) large = d > 51.0 factor[small] = 1.24 * d[small] ** -0.107 factor[large] = 1.51 * d[large] ** -0.157 return factor
[docs] def endurance_limit( self, diameter: FloatArray | float, load_factor: float = 1.0 ) -> FloatArray: """Corrected endurance limit ``S_e`` [MPa]. Args: diameter: Section diameter [mm], entering through ``k_b``. load_factor: ``k_c``; 1.0 bending, 0.85 axial, 0.59 torsion. Use 1.0 where bending dominates, which it does for every link here. Returns: ``S_e``, elementwise in ``diameter``. """ return ( self.surface_factor * self.size_factor(diameter) * load_factor * self.temperature_factor * self.reliability_factor * self.endurance_limit_uncorrected )
[docs] @dataclass(frozen=True) class SafetyFactors: """Design factors applied to each failure mode.""" static: float = 1.5 """``n_y`` against first yield.""" fatigue: float = 2.0 """``n_f`` on the Goodman line.""" buckling: float = 3.0 """``n_b`` against Euler buckling of members in compression. Higher than the others because Euler buckling is sudden, and because a real link has imperfections the ideal formula ignores. """
DEFAULT_MATERIAL = Material() DEFAULT_SAFETY = SafetyFactors()
[docs] def goodman_utilisation( alternating: FloatArray, mean: FloatArray, endurance: FloatArray, ultimate: float, ) -> FloatArray: """Goodman utilisation; ``<= 1 / n_f`` is safe. Args: alternating: ``sigma_a`` [MPa], non-negative. mean: ``sigma_m`` [MPa], signed. endurance: ``S_e`` [MPa]. ultimate: ``S_u`` [MPa]. Returns: ``sigma_a / S_e + max(sigma_m, 0) / S_u``. A compressive mean stress is not credited as beneficial: the Goodman line is truncated at ``sigma_m = 0``, which is the conservative convention and matters here because the connecting members swing between tension and compression every revolution. """ return alternating / endurance + np.maximum(mean, 0.0) / ultimate