Skip to content

Fix instability of force-length coupling in electromechanics, and uniform formulation of active stress tensor - #650

Open
michelebucelli wants to merge 23 commits into
SimVascular:mainfrom
michelebucelli:experiment/implicit-force-length-feedback-newton
Open

michelebucelli wants to merge 23 commits into
SimVascular:mainfrom
michelebucelli:experiment/implicit-force-length-feedback-newton

Conversation

@michelebucelli

Copy link
Copy Markdown
Collaborator

Fixes #635; fixes #641.

Current situation

  1. Different passive material models apply the active stress in different ways, although in principle active and passive models could be fully independent (see discussion on Uniform application of active stress across material models #635);
  2. Explicit coupling between active stress and structural mechanics through the fiber stretch $\lambda$ may give rise to temporal instabilities (see discussion on Temporal instability in electromechanics force-fiber-stretch feedback #641).

Release Notes

The PR addresses both issues above. Active tension, in particular, is now evaluated implicitly, and appropriate terms are added to the (u)struct matrices to account for the coupling. A detailed list of changes follows.

  1. Modified compute_pk2cc from mat_models.cpp so that all material models use the same formulation for the active stress tensor.

  2. Added the purely virtual function ActiveStress::compute_active_tension_derivative_local to compute the partial derivative of the active tension $T_\text{act}$ with respect to fiber stretch $\lambda$. The function was implemented in concrete models, with all models returning 0.0 except for ActiveStressRegazzoni.

  3. Added the helper function ActiveStress::compute_tension, to return active tensions along principal directions and their derivatives with respect to $\lambda$, bundled in an object of the new type ActiveStress::ActiveTension, introduced to be easily passed to downstream functions.

  4. The previous code computed active tension at integration points by

    1. projecting $\lambda$ from integration points to mesh nodes with a lumped $L^2$ projection;
    2. computing $T_\text{act}$ at mesh nodes;
    3. interpolating $T_\text{act}$ at integration points.

    In this framework, computing the tangent contribution of active stress would have required computing the derivative of $T_\text{act}$ wrt displacement, which would have meant differentiating through the $L^2$ projection. This would be a nonlocal, and thus nontrivial, operation.

    The new code evaluates $\lambda$ and $T_\text{act}$ directly at integration points. The nodal evaluation of $T_\text{act}$ is kept for the sake of output.

  5. Added helper class ActiveStress::evaluator to compute active tension and its derivative at integration points.

  6. Modified compute_pk2cc from mat_models.cpp to also account for the derivative of active tension when computing the tensor Dm. This also required modifying cc_to_voigt_eigen to relax its symmetry assumption.

  7. By default, the active stress model state is still advanced explicitly (i.e. implicit coupling is only used to evaluate the active tension function). Optionally, it can be made implicit by setting the XML parameter Active_stress/Implicit_state_coupling to true. This coupling does not contribute to the tangent, and is thus a fixed-point coupling between the two models. I implemented this experimentally, but opted to keep it in view of exploring its effect on force-velocity coupling stability (see Fiber stretch rate always evaluated to zero for active stress coupling #639).

  8. Refactoring of Integrator to accomodate for the implicit coupling.

  9. Updated all downstream consumers of active stress to accomodate for changed interfaces.

Documentation

All modified/new classes or functions were given Doxygen documentation.

Testing

A few tests (electromechanics/slab/Regazzoni, struct/tensile_adventitia_Guccione_active, ustruct/LV_Guccione_active) failed due to the change to the active stress formulation. I have regenerated their reference solution, as per the discussion in #635.

Besides that, all automatic tests are passing.

Additional context

  1. In principle, the uniform formulation of active stress and the instabilities are independent changes; however, fixing the instabilities required evaluating the tangent of active stress, for which having a uniform formulation was helpful. This is why the two changes are merged into a single PR.
  2. A unified formulation for active stress will also be beneficial when we will introduce the option to switch between different active stress tensor definitions, as discussed here.

Code of Conduct & Contributing Guidelines

@michelebucelli
michelebucelli requested review from aabrown100-git, dseyler, javijv4 and kko27 and a lite review from Copilot September 21, 2026 16:11
@michelebucelli michelebucelli self-assigned this Sep 21, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Unresolved critical restart-state and directional-validation issues, along with moderate solver coupling inconsistencies.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity · 1 Medium severity

Open (3)
What changed in this PR

This pull request standardizes active-stress formulation and improves electromechanical coupling stability through quadrature-point evaluation, stretch derivatives, and optional implicit state coupling.

Changes:

  • Unifies active-stress evaluation across material models.
  • Adds active-tension derivatives and quadrature-point evaluators.
  • Updates solver integration, tangents, postprocessing, tests, and reference outputs.
File Reviewed change / status
tests/​unitTests/​material_model_tests/​test_material_common.h Updates material-test helpers for the new active-stress API.
tests/​cases/​ustruct/​LV_Guccione_active/​result_001.vtu Regenerated regression reference output.
tests/​cases/​struct/​tensile_adventitia_Guccione_active/​result_002.vtu Regenerated regression reference output.
tests/​cases/​electromechanics/​slab/​result_Regazzoni_001.vtu Regenerated regression reference output.
Code/​Source/​solver/​ustruct.h Integrates active stress into incompressible assembly interfaces.
Code/​Source/​solver/​ustruct.cpp Passes evaluated active stress into incompressible assembly.
Code/​Source/​solver/​sv_struct.h Integrates active stress into structural assembly interfaces.
Code/​Source/​solver/​sv_struct.cpp Passes evaluated active stress into structural assembly.
Code/​Source/​solver/​post.cpp Uses quadrature-point active stress during postprocessing.
Code/​Source/​solver/​Parameters.h Declares implicit state-coupling configuration.
Code/​Source/​solver/​Parameters.cpp Implements implicit state-coupling configuration.
Code/​Source/​solver/​mat_models.h Updates constitutive active-stress APIs.
Code/​Source/​solver/​mat_models.cpp Applies the unified active stress and tangent. Critical (2 votes): sheet-normal stress with one fiber can access fl.col(1) without validating the number of fiber directions. Nit (1 vote): add nonzero active-tension finite-difference tangent coverage.
Code/​Source/​solver/​Integrator.h Declares stretch and active-state coupling helpers.
Code/​Source/​solver/​Integrator.cpp Integrates stretch computation and implicit state updates. Moderate (2 votes): stretch rate is computed before the displacement predictor, disabling Regazzoni force–velocity feedback. Moderate (1 vote): final Newton correction can leave mechanics and active state inconsistent.
Code/​Source/​solver/​fsi.cpp Updates FSI structural assembly interfaces.
Code/​Source/​solver/​ActiveStressUniformUnsteady.h Implements the zero direct stretch derivative.
Code/​Source/​solver/​ActiveStressUniformSteady.h Implements the zero direct stretch derivative.
Code/​Source/​solver/​ActiveStressRegazzoni.h Declares Regazzoni stretch-derivative support.
Code/​Source/​solver/​ActiveStressRegazzoni.cpp Implements Regazzoni stretch-derivative support.
Code/​Source/​solver/​ActiveStressNashPanfilov.h Implements the zero direct stretch derivative.
Code/​Source/​solver/​ActiveStress.h Defines active-tension data, evaluator, and lifecycle APIs.
Code/​Source/​solver/​ActiveStress.cpp Implements state gathering and tension evaluation. Critical (1 vote): restart and bin-to-VTK paths do not restore model state arrays, so freshly initialized state may be used instead of checkpoint state.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Code/Source/solver/ActiveStress.cpp
Comment thread Code/Source/solver/mat_models.cpp
Comment thread Code/Source/solver/Integrator.cpp
@codecov

codecov Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.30672% with 197 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.62%. Comparing base (c9838bd) to head (a921e78).

Files with missing lines Patch % Lines
Code/Source/solver/post.cpp 72.35% 175 Missing ⚠️
Code/Source/solver/Integrator.cpp 91.54% 6 Missing ⚠️
Code/Source/solver/ActiveStressRegazzoni.cpp 71.42% 4 Missing ⚠️
Code/Source/solver/ActiveStressUniformSteady.h 0.00% 2 Missing ⚠️
Code/Source/solver/initialize.cpp 0.00% 2 Missing ⚠️
Code/Source/solver/sv_struct.cpp 81.81% 2 Missing ⚠️
Code/Source/solver/ustruct.cpp 77.77% 2 Missing ⚠️
...s/active_stress_tests/active_stress_test_helpers.h 86.66% 2 Missing ⚠️
Code/Source/solver/fsi.cpp 80.00% 1 Missing ⚠️
Code/Source/solver/mat_models.cpp 97.72% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #650      +/-   ##
==========================================
+ Coverage   73.41%   73.62%   +0.20%     
==========================================
  Files         269      269              
  Lines       40337    40335       -2     
  Branches     6755     6744      -11     
==========================================
+ Hits        29612    29695      +83     
+ Misses      10482    10397      -85     
  Partials      243      243              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread Code/Source/solver/Integrator.cpp Outdated
Comment thread Code/Source/solver/mat_models.cpp Outdated

@kko27 kko27 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.

@michelebucelli Thanks for making these changes! This definitely simplifies many of the function signatures. I mostly have clarification questions and comments related to future PRs.

Comment thread Code/Source/solver/CepMod.h Outdated

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.

@michelebucelli do you know why we have this cemModelType class? It looks like legacy code for stretch activated currents...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Originally, this class bundled the parameters for active stress and active strain.

The active stress configuration was moved to ActiveStress and its derived classes, so I removed it from here.

The active strain configuration is left here, but it seems it is inaccessible to the user, that is there's no way to set cemModelType::aStrain to true. The same goes for cemModelType::cpld.

There's a few places in the code where these two flags are read in if statements, and the code they guard is mostly unreachable because of this.

I think we can do one of two things:

  1. we keep everything as it currently is, waiting for the day when support for active strain will be restored;
  2. we remove the class and all the dead code its flags are guarding, and if that code is needed when active strain will be reimplemented we rely on git history to retrieve it.

I prefer option 2, because the resulting code is less misleading (e.g. does not suggest to the reader/new developer that active strain is supported when it is not).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I also noticed that cemModelType::cpld also guards branches in cep_3d and cep_2d that modified the diffusion tensor to account for deformation. Those branches are currently dead code, since cpld can never be set to true, so I'm still in favor of dropping them.

Also, in light of this paper and my previous experience, those changes to the conduction tensor are not particularly significant and can generally be safely neglected (unlike e.g. stretch-activated currents). Still, should we want to bring them back, I think this should be done in a cleaner way by externalizing the evaluation of the deformation gradient (rather than implementing it again in cep_3d and cep_2d), so the current code would need to be reworked anyways.

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.

@michelebucelli Thanks! This makes sense. I'm in favor of removing the cem class and the evaluation of the deformation gradients. @aabrown100-git would you be okay with this? I think you added comments related to the active strain formulation in mat_models.cpp.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have tentatively removed the class in bf75a63, pending Aaron's opinion on this.

I restored SACs since that was a one-line change, but I removed the dead code for active strain and stretch-dependence of the conductivity tensor.

Comment thread Code/Source/solver/post.cpp
Comment thread Code/Source/solver/Integrator.cpp
Comment thread Code/Source/solver/Integrator.cpp
Comment thread tests/cases/electromechanics/slab/result_Regazzoni_001.vtu
double Ja;
mat_models::compute_pk2cc(com_mod, cep_mod, dmn, F, nFn, fN, ya_g_f, ya_g_s,
ya_g_n, S, Dm, Ja);
mat_models::compute_pk2cc(com_mod, cep_mod, dmn, F, nFn, fN, Ta, S, Dm, Ja);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we move the active stress contribution outside mat_models::compute_pk2cc? I think this would be beneficial because it would isolate the use of ' mat_models ' to only passive constitutive relationships, and because then adding the active component would follow a similar logic to adding the viscous terms (S = S + Svis), which would make the different contributions to PK2 very clear in the struct_* functions.

That being said, I am guessing the reason not to do this is to avoid recomputing the structural tensors Hff, Hss, Hnn. If the loss in time is too important, maybe we should leave it as is and revisit this once the struct/ustruct physics are refactored.

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. @michelebucelli and I discussed adding a general evaluator class for evaluating different quantities at integration points. I think it could be nice to be able to separate struct3d into kinematics (Nx, vx, F), passive stress, active stress, and viscosity, and prestress, since all of these contribute linearly to S.

As for recomputing Hff, Hss, and Hnn, is there any reason we need to store the fiber vectors at all or can we just store the structural tensors immediately after the fiber files are read? For interpolation of fiber direcitons, it makes sense to use the structure tensors anyway, and if we do need to access the vector itself for any reason, we can just compute the primary eigenvector of Hff, etc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I hadn't thought too much about this, but it's probably a good idea.

I think that long term the function compute_pk2cc should be retired and replaced by something like passive_model->compute_stress(F, Hff, Hss, Hnn, ...), where passive_model is an abstract constitutive model class (see also #178). This way, passive and active stress would be clearly separated, and all kinematics- and geometry-related quantities (F, Hff etc.) would be computed upstream (e.g. by struct_*) and then forwarded to the evaluators for the different contributions.

I think we can already start moving in that direction by splitting active stress out of this function. However, this relates to multiple issues/PRs currently open (at least this one and #640, and the evaluator class follow up I was discussing with @dseyler).

I think the evaluator class PR that will follow will be a good place to make this change.

@javijv4

javijv4 commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

@michelebucelli, another question: this PR adds derivatives to the active stress models. Should we add tests for these terms as well? I think we can do this in another PR, but it might be good to include this in future plans.

Comment thread Code/Source/solver/ActiveStress.cpp
@michelebucelli

Copy link
Copy Markdown
Collaborator Author

@javijv4

this PR adds derivatives to the active stress models. Should we add tests for these terms as well? I think we can do this in another PR, but it might be good to include this in future plans.

Good point, I will see if I can add something for this to the contraction model unit tests.

The flags the class bundled were never set to true, so the class was guarding dead code everywhere.
This dead code related to active strain (currently unsupported), changing conductivity in cep based
on deformation, and stretch-activated currents. I have only restored support for stretch-activated
currents (currently not covered by any automatic test).
@michelebucelli

Copy link
Copy Markdown
Collaborator Author

@javijv4 I've added a check in the unit test that compares the output of ActiveStress::compute_active_tension_derivative with centered finite differences on ActiveStress::compute_active_tension. The test fails if they don't agree.

On top of this, the electromechanics tests also exercise compute_active_tension_derivative, although since it is called within nonlinear iterations it doesn't necessarily impact the solution very much.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Temporal instability in electromechanics force-fiber-stretch feedback Uniform application of active stress across material models

7 participants