Skip to content

Templated viscous stress functions + linear shape function fix - #640

Open
dseyler wants to merge 11 commits into
SimVascular:mainfrom
dseyler:perf/visc-stress-templated
Open

Templated viscous stress functions + linear shape function fix #640
dseyler wants to merge 11 commits into
SimVascular:mainfrom
dseyler:perf/visc-stress-templated

Conversation

@dseyler

@dseyler dseyler commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Current situation

Address issues #633 and #634, which involve templating viscosity models on nsd and skipping unnecessary computation when shape function gradients are constant across an element. These changes reduce assembly runtime by ~30% and fix a bug where shape function gradients were assumed to be uniform within wedge elements.

Release Notes

  • compute_visc_stress_potential are now templated on nsd in mat_models.cpp
  • Eigen views are implemented throughout viscosity models to keep intermediate array computations on the stack and condense several for-loops into one-line expressions.
  • compute_visc_stress_and_tangent dispatches to templated models by nsd
  • mat_symm and mat_dev are also nsd templated in mat_fun.h
  • Svis, Kvis_u, and Kvis_v are allocated outside of the Gauss point loop, written over for each Gauss point, and only zeroed when no viscosity model is defined for an element (sv_struct.cpp, ustruct.cpp, fsi.cpp) or when viscosity arrays can be reused.
  • recompute_visc flag is added to indicate when viscosity computation can be skipped (shape function gradient are uniform for all Gauss points within an element, so Kvis_u, Kvis_v, and Svis values can be reused).
  • compute_visc_stress_and_tangent skips viscosity computation when recompute_visc == False
  • default case added to compute_visc_stress_and_tangent so viscosity arrays are zeroed and do not carry the previous element's values when an element doesn't have a viscosity model defined
  • struct_2d, struct_3d, ustruct_2d_m, ustruct_3d_m now require four new arguments: Svis, Kvis_u, Kvis_v, and recompute_visc
  • compute_visc_stress_and_tangent now takes recompute_visc
  • Fixed bug where lShpF should be False for wedge elements.

Testing

All test cases pass. Eigen operations were verified in isolation to be within machine precision of the operations they replaced.

Code of Conduct & Contributing Guidelines

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

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.53968% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.08%. Comparing base (2148c18) to head (2db0a06).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
Code/Source/solver/mat_models.cpp 85.36% 6 Missing ⚠️
Code/Source/solver/mat_fun.h 50.00% 2 Missing ⚠️
Code/Source/solver/nn_elem_props.h 0.00% 1 Missing ⚠️
Code/Source/solver/sv_struct.cpp 80.00% 1 Missing ⚠️
Code/Source/solver/ustruct.cpp 85.71% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #640      +/-   ##
==========================================
- Coverage   73.14%   73.08%   -0.07%     
==========================================
  Files         259      259              
  Lines       39516    39514       -2     
  Branches     6673     6667       -6     
==========================================
- Hits        28904    28877      -27     
- Misses      10368    10393      +25     
  Partials      244      244              

☔ 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.

@aabrown100-git

Copy link
Copy Markdown
Collaborator

@dseyler the viscous stress performance improvements look good, but why is the wedge element shape function included in this PR? Also, is it possible to add a wedge element test case?

@dseyler

dseyler commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

@dseyler the viscous stress performance improvements look good, but why is the wedge element shape function included in this PR? Also, is it possible to add a wedge element test case?

@aabrown100-git Maybe I should have opened a 2nd PR for that, but it's just a one-line change that @ktbolt noted was mistranslated from Fortran. The connection between the two PRs is that the viscosity model now only computes once for the first gauss point of each linear element as the viscosity arrays are the same for all Gauss points in the element. This would have given incorrect results for wedge elements which had the wrong lShpF flag. Several other places in the code also skip unnecessary per-gauss point computation for linear elements and would have given incorrect results for wedge elements as well

Definitely a good idea to add some wedge element test cases in the future, as there are currently none. Do you know of anyone working with them?

@aabrown100-git

Copy link
Copy Markdown
Collaborator

I thought @msbazzi was working with them?

If you can come up with a test case, I would create a new PR with your bug fix and a test case.

@ktbolt

ktbolt commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

A user on the SV Forum just sent me a mesh that uses wedges for the boundary layer. svMultiPhysics fails reading the mesh though; I will investigate.

@dseyler

dseyler commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

@aabrown100-git @ktbolt Are you suggesting creating a wedge element test case to add to this PR, or reverting the bug fix and pushing this PR through without it, which would cause the viscosity model to produce incorrect results for wedge elements (as 30 other places in the codebase already do)?

I haven't worked with wedge elements before, so would defer to someone else if they already have a good wedge element case to add

@ktbolt

ktbolt commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

@dseyler Go ahead and merge this PR. I will create a separate Issue for adding a wedge element CI test and for fixing the problem reading in wedges if it is indeed a bug and not something wrong with the user's file.

@aabrown100-git aabrown100-git left a comment

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.

Approving!

@ktbolt

ktbolt commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

@aabrown100-git @dseyler Note that you can created wedges using vtkLinearExtrusionFilter.

@lassoan

lassoan commented Sep 9, 2026

Copy link
Copy Markdown

@aabrown100-git The new "CFD mesh generator" module in VMTK Slicer extension can generate wedges as boundary layers (with multiple iterations of extrusion, smoothing, detangling). This can be useful if you want to test on complex, realistic meshes.

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.

🟡 Changes recommended

The updated viscosity kernels introduce silent-failure paths and reduced singular-matrix error handling that can leave outputs stale or produce NaNs in edge cases.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR improves structural-mechanics assembly performance by templating viscous stress kernels on nsd and enabling reuse of viscous stress/tangent terms across Gauss points when shape-function gradients are constant, while also fixing an element-property bug for wedge elements (WDG) where gradients were incorrectly treated as constant.

Changes:

  • Refactors Newtonian/potential viscosity models in mat_models.cpp to nsd-templated implementations using Eigen stack-based views.
  • Plumbs Svis, Kvis_u, Kvis_v, and a recompute_visc flag through struct_*, ustruct_*, and FSI assembly loops to reuse viscosity computations for linear elements.
  • Fixes wedge element properties by setting lShpF=false so shape gradients/Jacobians are recomputed per Gauss point.
File summaries
File Description
Code/Source/solver/ustruct.h Extends ustruct_*_m APIs to accept viscous stress/tangent buffers plus recompute_visc.
Code/Source/solver/ustruct.cpp Allocates/reuses viscosity buffers across Gauss points and passes recompute_visc into viscosity computation.
Code/Source/solver/sv_struct.h Extends struct_2d/struct_3d APIs to accept viscosity buffers plus recompute_visc.
Code/Source/solver/sv_struct.cpp Allocates/reuses viscosity buffers across Gauss points and passes recompute_visc into viscosity computation.
Code/Source/solver/nn_elem_props.h Sets WDG lShpF=false to avoid incorrectly reusing shape gradients within wedge elements.
Code/Source/solver/mat_models.h Updates compute_visc_stress_and_tangent signature to include recompute_visc and improves its API doc.
Code/Source/solver/mat_models.cpp Implements nsd-templated viscosity kernels and conditional recomputation/dispatch by nsd.
Code/Source/solver/mat_fun.h Adds fixed-size Eigen overloads for mat_symm and mat_dev.
Code/Source/solver/fsi.cpp Allocates/reuses viscosity buffers and forwards them into solid element routines with recompute_visc.
Review details

Suppressed comments (1)

Code/Source/solver/mat_models.cpp:1759

  • Same dispatch issue as the Newtonian branch: if F.nrows() is not 2 or 3, the Potential case becomes a no-op and can leave Svis/Kvis_* stale.
        if (F.nrows() == 3) {
          compute_visc_stress_potential<3>(lDmn.solid_visc.mu, eNoN, Nx, vx, F, Svis, Kvis_u, Kvis_v);
        } else if (F.nrows() == 2) {
          compute_visc_stress_potential<2>(lDmn.solid_visc.mu, eNoN, Nx, vx, F, Svis, Kvis_u, Kvis_v);
        }
  • Files reviewed: 9/9 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 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/mat_models.cpp
Comment thread Code/Source/solver/mat_models.cpp
Comment thread Code/Source/solver/mat_models.cpp

@michelebucelli michelebucelli left a comment

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.

Thank you @dseyler! I've left some suggestions.

As a general comment, I appreciate that Eigen brings some nice performance improvements, and that some of these changes also add some efficiency by avoiding unnecessary repeated allocations. However, there's a risk that both these kinds of changes increase the amount of "noise" in the code, by which I mean that the functions not only do the mathematical computation that they are supposed to do, but also spend quite a few lines in data format conversion and general bookkeeping, which may obfuscate their meaning a bit.

Some of the comments below go into the direction of mitigating this kind of issue (they are of course very much open to debate). I think if we can figure out some nice solution to this, then it can be used in the other performance improvements that you've worked on.

}

if (g == 0 || !fs_1[0].lShpF) {
// Viscosity is constant at all Gauss points for linear elements

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.

Minor suggestion:

Suggested change
// Viscosity is constant at all Gauss points for linear elements
// Viscosity is constant at all Gauss points for linear tetrahedral elements

Linear hexahedral elements are actually bi- or trilinear, so I don't think that this would apply to them, and I think it's a good idea to leave a reminder in the comment.

(I know that technically those are not linear elements, but they are sometime called that, so it might be better to be redundantly explicit just in case).

* Fixed-size overload for the Eigen matrices used by the element kernels.
*
* @tparam nsd Number of spatial dimensions.
* @param[in] A Second order tensor.

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.

Very minor:

Suggested change
* @param[in] A Second order tensor.
* @param[in] A second order tensor.

Same for the function below.

Comment on lines +1564 to +1565
/// @brief Largest element node count the fixed-size views below allow (HEX27).
constexpr int MAX_ELEMENT_NODES = 27;

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.

I'm not sure I have a clean solution to this, but: this number depends on the "largest" local finite element basis supported by the library, and this information is not logically pertinent to this module (the material model evaluation) but to the finite element basis module.

I think having this constant here introduces possible unexpected bugs (e.g. if larger finite element spaces are added in the future), and in general introduces implicit coupling between the two modules (implicit in the sense that the coupling is somewhat hidden).

One somewhat cleaner solution to this, I think, would be to move this definition to the basis function module (source/Code/FE/Basis). I am not sure what the appropriate file for that would be, but maybe @zasexton has a clearer picture.

Comment on lines +1601 to +1603
Eigen::Map<const Matrix<nsd>> F_map(F.data());
Eigen::Map<const Matrix<nsd>> vx_map(vx.data());
Eigen::Map<const NodalMatrix<nsd>> Nx_map(Nx.data(), nsd, eNoN);

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.

If I understand this right, these lines are constructing Eigen views over the data stored by the arrays, so that the subsequent operations use Eigen for efficiency but don't do any copy.

If that is right,

  1. Would it make sense to change the code in the caller to directly use Eigen structures for the local data, instead of custom Arrays? This would be a somewhat larger change perhaps, but I think it might make sense, reduce the (presumably very small, if even detectable) overhead associated to the construction of these views, and make the code a bit simpler (this function would just compute the viscosity, without any boilerplate code related to the technicalities of conversion).
  2. Should you choose not to follow the above suggestion, I would recommend writing a small helper function to do this, as a form of synctactic sugar, to turn these lines into e.g. auto F_eigen = to_eigen_map(F).

A similar suggestion applies to compute_visc_stress_newton.

Comment on lines +1595 to +1598
template <int nsd>
void compute_visc_stress_potential(const double mu, const int eNoN, const Array<double>& Nx,
const Array<double>& vx, const Array<double>& F,
Array<double>& Svis, Array3<double>& Kvis_u, Array3<double>& Kvis_v) {

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.

If I interpret this right, you removed the definition of this function and of compute_visc_stress_newton from the header because they are not meant to be called from outside this file.

If that is correct, I think it is a good decision, but I would also place both functions in the anonymous namespace above (my understanding of anonymous namespaces is that, among other things, they're a way of specifying that a certain name should only have internal visibility).

Comment on lines +1740 to +1742
if (!recompute_visc) {
return;
}

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.

I think that it would be better if this check was performed by the caller of this function, rather than by the function itself.

In other words, I think this function should always do what its name says (compute the viscosity), and the caller, who is aware of the context, should be the one to decide whether the viscosity needs to be recomputed (and thus the function needs to be called) or not.

Comment on lines +232 to +233
Array<double> Svis(nsd,nsd);
Array3<double> Kvis_u(nsd*nsd,eNoN,eNoN), Kvis_v(nsd*nsd,eNoN,eNoN);

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.

It seems to me that these arrays are not used by this function itself, but rather they are only used as "scratch" writing space for struct_3d and struct_2d, and they are declared here to avoid repeated allocations.

One downside is that this forces us to declare variables in a scope where those variables are not meaningful, which might make it harder to interpret the code.

An alternative solution to that would be to declare these variables inside the functions that need them (struct_3d and struct_2d) but declare them as static. This way, they will not be reallocated between subsequent calls, but they will remain local to the function's scope. You might have to write a bit of logic to ensure that they always have the correct size between calls, but I think it could be a good solution.

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.

6 participants