"""
Uniaxial tension model family (round, rectangular, notched).
Concrete models in this module:
- RoundUniaxialTensionModel
- RectangularUniaxialTensionModel
- RoundNotchedTensionModel
"""
import numpy as np
from matcal.core.boundary_condition_calculators import (
get_displacement_function_from_load_displacement_data_collection,
get_displacement_function_from_strain_data_collection,
raise_required_fields_not_found_error,
)
from matcal.core.constants import (
DISPLACEMENT_KEY,
DISPLACEMENT_RATE_KEY,
ENG_STRAIN_KEY,
ENG_STRESS_KEY,
LOAD_KEY,
STRAIN_RATE_KEY,
)
from matcal.sierra.input_file_writer import SolidMechanicsUserOutput
from matcal.sierra.models.base import _SymmetricUniaxiallyLoadedModelBase
from matcal.cubit.geometry import (
RoundUniaxialTensionGeometry,
RectangularUniaxialTensionGeometry,
RoundNotchedTensionGeometry,
)
class _TensionDerivedModelBase(_SymmetricUniaxiallyLoadedModelBase):
"""
Shared base for gripped symmetric specimens (tension family and torsion family).
"""
_loading_bc_node_sets = ["ns_side_grip"]
_death_blocks = ["necking_section"]
_model_blocks = ["grip_section", "gauge_section", "necking_section"]
_temperature_blocks = ["gauge_section", "necking_section"]
_thermal_bc_nodesets = ["ns_side_grip"]
def __init__(self, material, executable="adagio", **kwargs):
_all_input_params = {"mesh_method": 3}
_all_input_params.update(kwargs)
super().__init__(material, executable=executable, **_all_input_params)
def _create_derived_user_output_blocks(self, state):
self._add_disp_outputs("extensometer_surf", 2)
self._add_load_outputs(self._loading_bc_node_sets[0], 4)
self._add_strain_outputs()
self._add_stress_outputs()
def _add_strain_outputs(self):
strain_output = SolidMechanicsUserOutput("global_strain", "extensometer_surf", "node set")
self._input_file._solid_mechanics_region.add_subblock(strain_output)
extensometer_len = self._current_state_geo_params["extensometer_length"]
strain_output.add_compute_global_from_expression(ENG_STRAIN_KEY, f"{DISPLACEMENT_KEY}/{extensometer_len};")
self._input_file._add_heartbeat_global_variable(ENG_STRAIN_KEY)
def _add_stress_outputs(self):
stress_output = SolidMechanicsUserOutput("global_stress", "ns_side_grip", "node set")
self._input_file._solid_mechanics_region.add_subblock(stress_output)
reference_area = self.reference_area
stress_output.add_compute_global_from_expression(
ENG_STRESS_KEY, f"{LOAD_KEY}/{reference_area};"
)
self._input_file._add_heartbeat_global_variable(ENG_STRESS_KEY)
class _UniaxialTensionModelBase(_TensionDerivedModelBase):
"""
Shared base for uniaxial tension models that compute engineering stress/strain
from displacement and load outputs.
"""
def _get_loading_boundary_condition_displacement_function(self, state, params_by_precedent):
bc_data = self._boundary_condition_data
extra_lines = []
# Allow STRAIN_RATE or DISPLACEMENT_RATE to be used interchangeably by deriving the other.
if STRAIN_RATE_KEY in params_by_precedent.keys():
disp_rate = (
params_by_precedent[STRAIN_RATE_KEY] *
self._current_state_geo_params.extensometer_length
)
params_by_precedent.update({DISPLACEMENT_RATE_KEY: disp_rate})
elif DISPLACEMENT_RATE_KEY in params_by_precedent.keys():
eng_strain_rate = (
params_by_precedent[DISPLACEMENT_RATE_KEY] /
self._current_state_geo_params.extensometer_length
)
params_by_precedent.update({STRAIN_RATE_KEY: eng_strain_rate})
common_state_field_names = bc_data.state_common_field_names(state.name)
# Choose how to interpret boundary condition data
if DISPLACEMENT_KEY in common_state_field_names:
scale_factor = (
self._current_state_geo_params.gauge_length /
self._current_state_geo_params.extensometer_length
)
disp_function, metadata = (
get_displacement_function_from_load_displacement_data_collection(
bc_data,
state,
params_by_precedent,
scale_factor=scale_factor,
return_metadata=True,
)
)
extra_lines.append(
f'Converted measured "{DISPLACEMENT_KEY}" to grip displacement using '
f'gauge_length / extensometer_length = {scale_factor}.'
)
elif ENG_STRAIN_KEY in common_state_field_names:
scale_factor = self._current_state_geo_params.gauge_length
disp_function, metadata = get_displacement_function_from_strain_data_collection(
bc_data,
state,
params_by_precedent,
scale_factor=scale_factor,
return_metadata=True,
)
extra_lines.append(
f'Converted "{ENG_STRAIN_KEY}" to displacement using gauge_length = {scale_factor}.'
)
else:
raise_required_fields_not_found_error(
state,
DISPLACEMENT_KEY + ", " + ENG_STRAIN_KEY,
bc_data.name,
)
# Account for symmetry across the gauge length (model is built with symmetry)
disp_function[DISPLACEMENT_KEY] *= 0.5
extra_lines.append("Applied symmetry factor of 0.5 to the prescribed displacement.")
self._set_last_loading_bc_comment(metadata, extra_lines)
return disp_function
def _create_derived_user_output_blocks(self, state):
super()._create_derived_user_output_blocks(state)
self._add_contraction_output()
@property
def reference_area(self):
raise NotImplementedError
def _add_contraction_output(self):
raise NotImplementedError
[docs]
class RoundUniaxialTensionModel(_UniaxialTensionModelBase):
"""
MatCal generated SIERRA/SM uniaxial tension test model with a round cross section.
"""
model_type = "round_uniaxial_tension_model"
_geometry_creator_class = RoundUniaxialTensionGeometry
def _add_contraction_output(self):
contraction_output_z = SolidMechanicsUserOutput(
"z_contraction", "z_radial_node", "node set"
)
self._input_file._solid_mechanics_region.add_subblock(contraction_output_z)
contraction_output_z.add_compute_global_from_nodal_field(
"z_radial_node_z_displacement", "displacement(z)"
)
contraction_output_z.add_compute_global_from_expression(
"z_contraction", "2*z_radial_node_z_displacement;"
)
self._input_file._add_heartbeat_global_variable("z_contraction")
contraction_output_x = SolidMechanicsUserOutput(
"x_contraction", "x_radial_node", "node set"
)
self._input_file._solid_mechanics_region.add_subblock(contraction_output_x)
contraction_output_x.add_compute_global_from_nodal_field(
"x_radial_node_x_displacement", "displacement(x)"
)
contraction_output_x.add_compute_global_from_expression(
"x_contraction", "2*x_radial_node_x_displacement;"
)
self._input_file._add_heartbeat_global_variable("x_contraction")
@property
def reference_area(self):
r = self._current_state_geo_params["gauge_radius"]
return np.pi * np.double(r) ** 2
[docs]
class RectangularUniaxialTensionModel(_UniaxialTensionModelBase):
"""
MatCal generated SIERRA/SM uniaxial tension test model with a rectangular cross section.
"""
model_type = "rectangular_uniaxial_tension_model"
_geometry_creator_class = RectangularUniaxialTensionGeometry
def _add_contraction_output(self):
contraction_output_z = SolidMechanicsUserOutput(
"z_contraction", "thickness_center_node", "node set"
)
self._input_file._solid_mechanics_region.add_subblock(contraction_output_z)
contraction_output_z.add_compute_global_from_nodal_field(
"thickness_node_z_displacement", "displacement(z)"
)
contraction_output_z.add_compute_global_from_expression(
"z_contraction", "2*thickness_node_z_displacement;"
)
self._input_file._add_heartbeat_global_variable("z_contraction")
contraction_output_x = SolidMechanicsUserOutput(
"x_contraction", "gauge_width_center_node", "node set"
)
self._input_file._solid_mechanics_region.add_subblock(contraction_output_x)
contraction_output_x.add_compute_global_from_nodal_field(
"width_node_x_displacement", "displacement(x)"
)
contraction_output_x.add_compute_global_from_expression(
"x_contraction", "2*width_node_x_displacement;"
)
self._input_file._add_heartbeat_global_variable("x_contraction")
@property
def reference_area(self):
return (
self._current_state_geo_params["gauge_width"] *
self._current_state_geo_params["thickness"]
)
[docs]
class RoundNotchedTensionModel(_TensionDerivedModelBase):
"""
MatCal generated SIERRA/SM notched tension test model with a round cross section.
"""
model_type = "round_notched_tension_model"
_geometry_creator_class = RoundNotchedTensionGeometry
@property
def reference_area(self):
r = self._current_state_geo_params["notch_gauge_radius"]
return np.pi * np.double(r) ** 2
def _get_loading_boundary_condition_displacement_function(self, state, params_by_precedent):
bc_data = self._boundary_condition_data
common_state_field_names = bc_data.state_common_field_names(state.name)
extra_lines = []
# Allow either displacement or engineering_strain as BC input
if DISPLACEMENT_KEY in common_state_field_names:
disp_function, metadata = (
get_displacement_function_from_load_displacement_data_collection(
bc_data,
state,
params_by_precedent,
scale_factor=1.0,
return_metadata=True,
)
)
elif ENG_STRAIN_KEY in common_state_field_names:
scale_factor = self._current_state_geo_params.extensometer_length
disp_function, metadata = get_displacement_function_from_strain_data_collection(
bc_data,
state,
params_by_precedent,
scale_factor=scale_factor,
return_metadata=True,
)
extra_lines.append(
f'Converted "{ENG_STRAIN_KEY}" to displacement using extensometer_length = {scale_factor}.'
)
else:
raise_required_fields_not_found_error(
state,
DISPLACEMENT_KEY + ", " + ENG_STRAIN_KEY,
bc_data.name,
)
# Account for symmetry across gauge section
disp_function[DISPLACEMENT_KEY] *= 0.5
extra_lines.append("Applied symmetry factor of 0.5 to the prescribed displacement.")
self._set_last_loading_bc_comment(metadata, extra_lines)
return disp_function