Skip to content

[GeoMechanicsApplicaiton] Make tension cut off truly optional for Mohr Coulomb model#14467

Open
rfaasse wants to merge 22 commits into
masterfrom
geo/14463-make-tension-cut-off-truly-optional
Open

[GeoMechanicsApplicaiton] Make tension cut off truly optional for Mohr Coulomb model#14467
rfaasse wants to merge 22 commits into
masterfrom
geo/14463-make-tension-cut-off-truly-optional

Conversation

@rfaasse

@rfaasse rfaasse commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

📝 Description

This PR enables the user to specify a Mohr Coulomb model with or without tension cut-off, using the new GEO_ENABLE_TENSION_CUT_OFF parameter in the material parameters. When the tension cut-off is turned off, the Mohr Coulomb model collapses to a simplified version with just the regular failure zone, elastic zone and apex return.

Key changes

  • The GEO_ENABLE_TENSION_CUT_OFF is added as a material parameter for the Mohr Coulomb model. If it's not specified, but a tensile strength is specified, it is assumed the tension cut-off is enabled (for backward compatibility).
  • Unit tests and integration tests are adjusted (GEO_ENABLE_TENSION_CUT_OFF with the correct value is added). For some tests, the tensile strength was specified as cohesion/tan(phi), which is equivalent to disabling the tension cut-off. For the tests where this was the case, the GEO_ENABLE_TENSION_CUT_OFF is set to false
  • Added equivalent registered names for "GeoMohrCoulombWithTensionCutOff2D", "GeoMohrCoulombWithTensionCutOff3D", "GeoInterfaceCoulombWithTensionCutOff" -> "GeoMohrCoulomb2D", "GeoMohrCoulomb3D", "GeoInterfaceCoulomb". The old names are kept for now, but deprecated (to be removed after some time).

Note: A PR for the user documentation will be created separately

@rfaasse rfaasse self-assigned this Jun 3, 2026
@rfaasse rfaasse linked an issue Jun 3, 2026 that may be closed by this pull request
Comment on lines +358 to +361
// Keeping the old names for backward compatibility
KRATOS_REGISTER_CONSTITUTIVE_LAW("GeoMohrCoulombWithTensionCutOff2D", mMohrCoulomb2D)
KRATOS_REGISTER_CONSTITUTIVE_LAW("GeoMohrCoulombWithTensionCutOff3D", mMohrCoulomb3D)
KRATOS_REGISTER_CONSTITUTIVE_LAW("GeoInterfaceCoulombWithTensionCutOff", mInterfaceCoulomb)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was doubting whether we should keep the old ones. Curious what others think

@rfaasse rfaasse added the GeoMechanics Issues related to the GeoMechanicsApplication label Jun 4, 2026
@rfaasse rfaasse marked this pull request as ready for review June 4, 2026 14:41
@rfaasse rfaasse requested a review from a team as a code owner June 4, 2026 14:41

@WPK4FEM WPK4FEM left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HI Richard,
This is what I managed to read and comment on today, I will have another look.
Wijtze Pieter

const auto coulomb_yield_function_value = mCoulombYieldSurface.YieldFunctionValue(rTrialStressState);
const auto tension_yield_function_value = mTensionCutOff.YieldFunctionValue(rTrialStressState);
constexpr auto tolerance = 1.0e-10;
constexpr auto tolerance = 1.0e-10;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please relate there to the available relative or absolute numerical tolerances.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are in test_utilities.h (so in the test folder) and therefore cannot be reached here. Also, the defaults are 1e-6 for relative and 1e-12 for absolute. What do you think we should use here? I wasn't that involved in the development of Coulomb, so I'm not sure why this exact tolerance was chosen at the time

Comment on lines +127 to +134
auto tension_admissable = true;
if (mTensionCutOff) {
const auto tension_yield_function_value = mTensionCutOff->YieldFunctionValue(rTrialStressState);
const auto tension_tolerance = tolerance * (1.0 + std::abs(tension_yield_function_value));
tension_admissable = tension_yield_function_value < tension_tolerance;
}

const auto admissible_state = coulomb_yield_function_value < coulomb_tolerance && tension_admissable;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MC part is always there so lets start from that

Suggested change
auto tension_admissable = true;
if (mTensionCutOff) {
const auto tension_yield_function_value = mTensionCutOff->YieldFunctionValue(rTrialStressState);
const auto tension_tolerance = tolerance * (1.0 + std::abs(tension_yield_function_value));
tension_admissable = tension_yield_function_value < tension_tolerance;
}
const auto admissible_state = coulomb_yield_function_value < coulomb_tolerance && tension_admissable;
auto admissible_state = coulomb_yield_function_value < coulomb_tolerance;
if (admissible_state && mTensionCutOff) {
const auto tension_yield_function_value = mTensionCutOff->YieldFunctionValue(rTrialStressState);
const auto tension_tolerance = tolerance * (1.0 + std::abs(tension_yield_function_value));
admissible_state = tension_yield_function_value < tension_tolerance;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice suggestion, done!

Comment on lines +151 to 160
if (mTensionCutOff && IsStressAtTensionApexReturnZone(trial_traction)) {
mPlasticityStatus = PlasticityStatus::TENSION_APEX;
return ReturnStressAtTensionApexReturnZone(rTrialStressState);
}

if (IsStressAtTensionCutoffReturnZone(trial_traction)) {
if (mTensionCutOff && IsStressAtTensionCutoffReturnZone(trial_traction)) {
mPlasticityStatus = PlasticityStatus::TENSION_CUT_OFF;
return ReturnStressAtTensionCutoffReturnZone(rTrialStressState,
rElasticConstitutiveTensor, AveragingType);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both previous conditions need the TensionCutOff to be active so lets check that in an encompassing condition.

Suggested change
if (mTensionCutOff) {
if (IsStressAtTensionApexReturnZone(trial_traction)) {
mPlasticityStatus = PlasticityStatus::TENSION_APEX;
return ReturnStressAtTensionApexReturnZone(rTrialStressState);
}
if (IsStressAtTensionCutoffReturnZone(trial_traction)) {
mPlasticityStatus = PlasticityStatus::TENSION_CUT_OFF;
return ReturnStressAtTensionCutoffReturnZone(rTrialStressState,
rElasticConstitutiveTensor, AveragingType);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this suggestion. 👍

@avdg81 avdg81 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Richard,
Thanks a lot for making the tension cut-off of the Mohr-Coulomb model truly optional. With your changes, there's no doubt about when a tension cut-off was intended or not. It also helps to structure the code even more.
I have several questions and suggestions. Hope all of them will help to make your valuable changes stand out even more. 👍

Comment on lines +61 to +69
if (rMaterialProperties.Has(GEO_ENABLE_TENSION_CUT_OFF) && rMaterialProperties[GEO_ENABLE_TENSION_CUT_OFF]) {
return std::make_optional<TensionCutoff>(rMaterialProperties[GEO_TENSILE_STRENGTH]);
}

// The following statement is to support backward compatibility (i.e. GEO_ENABLE_TENSION_CUT_OFF is not specified,
// but the GEO_TENSILE_STRENGTH is provided), which results in an enabled tension cutoff.
if (rMaterialProperties.Has(GEO_TENSILE_STRENGTH) && !rMaterialProperties.Has(GEO_ENABLE_TENSION_CUT_OFF)) {
return std::make_optional<TensionCutoff>(rMaterialProperties[GEO_TENSILE_STRENGTH]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the return statements are identical, I would suggest to join the condition checks and put them in a local helper function named WantTensionCutOff (or any other name if you can think of a better one):

Suggested change
if (rMaterialProperties.Has(GEO_ENABLE_TENSION_CUT_OFF) && rMaterialProperties[GEO_ENABLE_TENSION_CUT_OFF]) {
return std::make_optional<TensionCutoff>(rMaterialProperties[GEO_TENSILE_STRENGTH]);
}
// The following statement is to support backward compatibility (i.e. GEO_ENABLE_TENSION_CUT_OFF is not specified,
// but the GEO_TENSILE_STRENGTH is provided), which results in an enabled tension cutoff.
if (rMaterialProperties.Has(GEO_TENSILE_STRENGTH) && !rMaterialProperties.Has(GEO_ENABLE_TENSION_CUT_OFF)) {
return std::make_optional<TensionCutoff>(rMaterialProperties[GEO_TENSILE_STRENGTH]);
}
if (WantTensionCutOff(rMaterialProperties)) {
return std::make_optional<TensionCutoff>(rMaterialProperties[GEO_TENSILE_STRENGTH]);
}

where

bool WantTensionCutOff(const Properties& rMaterialProperties)
{
    if (rMaterialProperties.Has(GEO_ENABLE_TENSION_CUT_OFF) && rMaterialProperties[GEO_ENABLE_TENSION_CUT_OFF])
        return true;

    // The following statement is to support backward compatibility (i.e. GEO_ENABLE_TENSION_CUT_OFF is not specified,
    // but the GEO_TENSILE_STRENGTH is provided), which results in an enabled tension cutoff.
    return !rMaterialProperties.Has(GEO_ENABLE_TENSION_CUT_OFF) && rMaterialProperties.Has(GEO_TENSILE_STRENGTH);
}

As you can see, I have also swapped the two conditions in the backward compatibility check. In that way, the check whether the flag GEO_ENABLE_TENSION_CUT_OFF exists always comes first, which is a little bit clearer for me.


template <typename StressStateType>
bool CoulombWithTensionCutOffImpl::IsAdmissibleStressState(const StressStateType& rTrialStressState)
bool CoulombImpl::IsAdmissibleStressState(const StressStateType& rTrialStressState)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we can extract the checking whether the given trial stress state is admissible for a given yield surface to a new helper function, and then use that to implement this member function.

In the anonymous we can add the following function template:

template <typename YieldSurfaceType, typename StressStateType>
bool IsAdmissibleStressState(const YieldSurfaceType& rYieldSurface, const StressStateType& rTrialStressState)
{
    const auto yield_function_value = rYieldSurface.YieldFunctionValue(rTrialStressState);
    const auto tolerance            = 1.0e-10 * (1.0 + std::abs(yield_function_value));
    return yield_function_value < tolerance;
}

And then the member function template can be simplified as follows:

template <typename StressStateType>
bool CoulombImpl::IsAdmissibleStressState(const StressStateType& rTrialStressState)
{
    if (!::IsAdmissibleStressState(mCoulombYieldSurface, rTrialStressState)) return false;

    if (mTensionCutOff && !::IsAdmissibleStressState(*mTensionCutOff, rTrialStressState))
        return false;

    mPlasticityStatus = PlasticityStatus::ELASTIC;
    return true;
}

Note that with this new structure in-place, adding the optional cap yield surface becomes straightforward.

Comment on lines +151 to 160
if (mTensionCutOff && IsStressAtTensionApexReturnZone(trial_traction)) {
mPlasticityStatus = PlasticityStatus::TENSION_APEX;
return ReturnStressAtTensionApexReturnZone(rTrialStressState);
}

if (IsStressAtTensionCutoffReturnZone(trial_traction)) {
if (mTensionCutOff && IsStressAtTensionCutoffReturnZone(trial_traction)) {
mPlasticityStatus = PlasticityStatus::TENSION_CUT_OFF;
return ReturnStressAtTensionCutoffReturnZone(rTrialStressState,
rElasticConstitutiveTensor, AveragingType);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this suggestion. 👍

Geo::SigmaTau CoulombImpl::CalculateCornerPoint() const
{
const auto tensile_strength = mTensionCutOff.GetTensileStrength();
if (const auto apex = mCoulombYieldSurface.CalculateApex(); !mTensionCutOff) return apex;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since apex is not being used by the check itself, I feel we'd better postpone its calculation to the latest possible moment:

Suggested change
if (const auto apex = mCoulombYieldSurface.CalculateApex(); !mTensionCutOff) return apex;
if (!mTensionCutOff) return mCoulombYieldSurface.CalculateApex();

(And let's add an additional blank line to clearly show that we've handled the first case.)

std::size_t mMaxNumberOfPlasticIterations{100};
PlasticityStatus mPlasticityStatus{PlasticityStatus::ELASTIC};
CoulombYieldSurface mCoulombYieldSurface;
std::optional<TensionCutoff> mTensionCutOff;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a name like mOptionalTensionCutOff better reflect that the tension cut-off is optional? In other words, that it may or may not be there.

Comment on lines +325 to 326
"GEO_ENABLE_TENSION_CUT_OFF": true,
"GEO_TENSILE_STRENGTH": 0.0,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"GEO_ENABLE_TENSION_CUT_OFF": true,
"GEO_TENSILE_STRENGTH": 0.0,
"GEO_ENABLE_TENSION_CUT_OFF": false,

Comment on lines +363 to +365
KRATOS_REGISTER_CONSTITUTIVE_LAW("GeoMohrCoulomb2D", mMohrCoulomb2D)
KRATOS_REGISTER_CONSTITUTIVE_LAW("GeoMohrCoulomb3D", mMohrCoulomb3D)
KRATOS_REGISTER_CONSTITUTIVE_LAW("GeoInterfaceCoulomb", mInterfaceCoulomb)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new names are slightly different than what was proposed in the issue:

"GeoMohrCoulombWithTensionCutOff2D" => "GeoMohrCoulombLawPlaneStrain"
"GeoMohrCoulombWithTensionCutOff3D" => "GeoMohrCoulombLaw3D"
"GeoInterfaceCoulombWithTensionCutOff" => "GeoInterfaceCoulombLawPlaneStrain"

I'm wondering why the names are somewhat different?


const MohrCoulombWithTensionCutOff mMohrCoulombWithTensionCutOff2D{std::make_unique<PlaneStrain>()};
const MohrCoulombWithTensionCutOff mMohrCoulombWithTensionCutOff3D{std::make_unique<ThreeDimensional>()};
const MohrCoulomb mMohrCoulomb2D{std::make_unique<PlaneStrain>()};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel mMohrCoulombPlaneStrain is a better name (or perhaps even better: mMohrCoulombLawPlaneStrain). Perhaps @WPK4FEM wants to weigh in here, too?

Comment on lines -654 to -655
const auto tensile_strength = cohesion / std::tan(MathUtils<>::DegreesToRadians(phi_in_degrees));
properties.SetValue(GEO_TENSILE_STRENGTH, tensile_strength);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's clearer to be explicit about not wanting a tension cut-off rather than relying on a default value:

        properties.SetValue(GEO_ENABLE_TENSION_CUT_OFF, false);

// Act
auto cauchy_stress_vector = UblasUtilities::CreateVector(
{tensile_strength + 20.0, tensile_strength + 10.0, tensile_strength, 0.0});
const auto sigma_corner = cohesion / std::tan(MathUtils<>::DegreesToRadians(phi_in_degrees));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps apex is a better name?

Suggested change
const auto sigma_corner = cohesion / std::tan(MathUtils<>::DegreesToRadians(phi_in_degrees));
const auto apex = cohesion / std::tan(MathUtils<>::DegreesToRadians(phi_in_degrees));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

GeoMechanics Issues related to the GeoMechanicsApplication

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[GeoMechanicsApplication] Make tension cut-off truly optional

3 participants