torch-fem
Authors: Meyer, Nils
Type: Software
Published: 2026-08-03
Keywords: finite elements, differentiable simulation, pytorch, topology optimization

<h3>Added</h3>
<ul>
<li>A PEP 561 <code>py.typed</code> marker, so downstream type checkers use the shipped annotations.</li>
<li>Tests for the conductivity materials, the orthotropic plane-stress and plane-strain materials, <code>linear_to_quadratic(...)</code>, <code>Truss</code>, <code>SolidHeat</code>, the boundary condition setters, and the non-planar mesh imports, none of which had unit tests before.</li>
<li>A first-order patch test over all eight supported element types. <code>Planar</code> and <code>Solid</code> were previously only ever tested with <code>Quad1</code> and <code>Hexa1</code> meshes.</li>
<li><code>torchfem.sparse.resolve_method(...)</code> returns the linear solver backend that <code>sparse_solve(...)</code> picks for a given system size and device. <code>sparse_solve(...)</code> now uses it instead of its own copy of the rules.</li>
<li><code>Solid.plot(..., clip=("rho", 0.5))</code> cuts the mesh at an iso-value of a property, culling orientations and boundary conditions with it. The <code>optimization/solid/bracket.ipynb</code> example uses it to show the optimized part instead of exporting a VTU for ParaView.</li>
<li><code>Solid.plot(..., show_outline=True)</code> draws a box around the full mesh, which gives a clipped result its design space back.</li>
<li><code>Shell.plot(..., plotter=pl)</code> and <code>Truss.plot3d(..., plotter=pl)</code> render into an existing PyVista plotter instead of creating and showing their own, as <code>Solid.plot(...)</code> already did.</li>
<li><code>bcs</code> on <code>Truss.plot2d(...)</code> and <code>Truss.plot3d(...)</code>, which drew boundary conditions unconditionally. It defaults to True, matching <code>Planar.plot(...)</code>, so plots are unchanged unless it is switched off.</li>
<li><code>Shell.plot(..., orientations=...)</code> draws per-element direction vectors, e.g. the local frames <code>shell.t</code>, as red, green, and blue arrows, like <code>Solid.plot(...)</code> does. On a shell drawn with <code>thickness=True</code> they sit on the top surface instead of inside it. The <code>optimization/shell/orientation.ipynb</code> example uses it to show the optimized fiber directions in 3D.</li>
<li><code>Shell.plot(..., show_undeformed=True)</code> for consistency.</li>
<li><code>Solid.plot(..., orientations=...)</code> accepts fewer than three vectors per element, like <code>Shell.plot(...)</code>, so <code>[n_elem, 1, 3]</code> draws a fiber direction alone. The <code>optimization/solid/topology+orientation.ipynb</code> example passes just that instead of the full rotated frame.</li>
<li>Docstrings for <code>Truss.plot2d(...)</code> and <code>Truss.plot3d(...)</code>, which had none.</li>
</ul>
<h3>Changed</h3>
<ul>
<li>The <code>notebook</code> extra requires <code>pyvista[jupyter]</code> instead of listing <code>trame</code>, <code>trame-vtk</code>, and <code>trame-vuetify</code> itself, so the trame versions stay inside the range PyVista supports. A newer <code>trame-vtk</code> ships a VTK.js bundle that PyVista's HTML Jupyter backend embeds incorrectly, leaving the plot blank.</li>
<li><code>Solid.plot(...)</code> passes <code>algorithm=None</code> to <code>extract_surface(...)</code>, silencing a PyVista warning about its default changing. The extracted surface is unchanged.</li>
<li><code>Shell.plot(..., mirror=...)</code> no longer draws the constraint cones that enforce the symmetry of a mirrored plane, since the mirrored copy already shows that symmetry, and warns if the nodes on such a plane are not symmetry-constrained. Loads and all other constraints on the plane are still drawn.</li>
<li><code>Shell.plot(...)</code> and <code>Truss.plot3d(...)</code> no longer set the global PyVista Jupyter backend to <code>client</code>. Both pass <code>jupyter_backend="html"</code> to <code>show(...)</code>, which overrode it anyway, so only the global side effect on other plots is gone.</li>
<li><code>verbose=True</code> in <code>solve(...)</code> and <code>time_integration(...)</code> prints a compact table with one row per increment, holding its substeps, iterations, residual, wall time, and flags counting the substep cutbacks (<code>↓</code>) and growths (<code>↑</code>), under a header naming the model, the machine, the linear solver backend actually used, and the Newton settings. Notebooks redraw the table in place, elsewhere rows stream as they complete. Iterations count linear solves, so a linear problem needs exactly one.</li>
<li><strong>Breaking:</strong> The <code>verbose</code> argument of <code>torchfem.sparse.newton_solve(...)</code> became <code>report</code>, taking a <code>torchfem.report.SolveReport</code> or <code>None</code> instead of a bool.</li>
<li>Linting and formatting moved from <code>black</code>, <code>isort</code>, and <code>flake8</code> to <code>ruff</code>, configured in <code>pyproject.toml</code> under <code>[tool.ruff]</code>. Local checks are now <code>ruff format .</code> and <code>ruff check --fix .</code>. Ruff also lints the example notebooks, which the previous stack never covered.</li>
<li>CI measures test coverage with <code>pytest-cov</code> and fails below 78% (currently 81%).</li>
<li>The <code>increments</code> argument of <code>solve(...)</code> and the <code>t_output</code> argument of <code>time_integration(...)</code> now default to <code>None</code> instead of a <code>torch.tensor([0.0, 1.0])</code> built once at import. The effective default is unchanged.</li>
<li>The <code>optimization/solid/bracket.ipynb</code> example interpolates stiffness as <code>C_min + rho^p (C0 - C_min)</code> with <code>C_min = 1e-3 C0</code>, instead of <code>rho^p C0</code>. The stiffness floor no longer depends on the density bound, so <code>rho_min</code> drops from 0.01 to 1e-3 and less of the volume budget is spent on void.</li>
<li><strong>Breaking:</strong> <code>G_13</code> and <code>G_23</code> of <code>OrthotropicElasticityPlaneStress</code> and <code>OrthotropicElasticityPlaneStrain</code> are unset instead of defaulting to <code>0.0</code>, and a homogeneous <code>Shell(...)</code> integrates them into its transverse shear stiffness when <code>transverse_G</code> is not given. A <code>Laminate</code> of plies without transverse moduli silently integrated to zero transverse shear stiffness before and now raises.</li>
</ul>
<h3>Removed</h3>
<ul>
<li><strong>Breaking:</strong> The <code>contour</code> argument of <code>Solid.plot(...)</code>, superseded by <code>clip</code>. The <code>basic/solid/gyroid.ipynb</code> example now clips on <code>thickness - sdf.abs()</code>, rendering the wall as a solid instead of its two bounding surfaces.</li>
<li><strong>Breaking:</strong> The <code>threshold_condition</code> argument of <code>Solid.plot(...)</code>, superseded by <code>clip</code>.</li>
<li><strong>Breaking:</strong> The <code>torchfem.sdfs</code> module, which provided signed distance functions for implicit geometry (TPMS surfaces, primitives, and CSG booleans) and is out of scope for a finite element library. The <code>basic/solid/gyroid.ipynb</code> example now defines its distance function inline.</li>
<li>The <code>basic/solid/implicits.ipynb</code> and <code>basic/solid/tpms.ipynb</code> examples and their gallery entries.</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><code>solve(...)</code> subdivided every increment of a load path with growing increments. The attempted substep is now carried across increments as a fraction of an increment instead of an absolute size.</li>
<li><code>solve(...)</code> never recovered to one substep per increment after a cutback. It grew the substep from <code>step</code>, which is clipped so the substep lands exactly on the requested increment, instead of from the size it asked for. Every increment therefore ended by shrinking the substep to <code>growth_factor</code> times its own last remainder, and kept subdividing ever more finely for the rest of the load path. The <code>basic/planar/stabilization.ipynb</code> snap-through now takes 126 Newton iterations instead of 168.</li>
<li><code>__repr__</code> of <code>Truss</code>, <code>Planar</code>, <code>Solid</code>, and <code>Shell</code> reported the element type as <code>ABCMeta</code>. It read <code>self.etype.__class__.__name__</code>, but <code>etype</code> is already a class, so this gave the name of its metaclass.</li>
<li><code>rotate(...)</code> raised <code>IndexError</code> on <code>OrthotropicElasticityPlaneStrain</code> and returned meaningless <code>E_1</code>, <code>E_2</code>, <code>nu_12</code>, and <code>G_12</code> on <code>OrthotropicElasticityPlaneStress</code>. Both inverted the fourth-order stiffness tensor instead of its Voigt matrix, and now use <code>stiffness2voigt(self.C)</code> like the 3D class. The rotated <code>C</code> was always correct.</li>
<li>The <code>IsotropicConductivity3D</code> docstring documented a non-existent attribute <code>k</code> (it is <code>kappa</code>) and described <code>step(...)</code> as a small-strain elasticity model.</li>
<li>Several functions shared a single mutable default across all calls: <code>cached_solve=CachedSolve()</code> in <code>sparse_solve(...)</code>, <code>newton_solve(...)</code> and their autograd wrappers, <code>nodal_data</code>/<code>elem_data</code> in <code>export_mesh(...)</code>, and two arguments of <code>plot_contours(...)</code>. Each default is now built per call.</li>
<li>The <code>psi</code> function in the <code>basic/solid/large_compression.ipynb</code> example read the module-level <code>mu</code> and <code>lbd</code> instead of its <code>params</code>, which would have zeroed gradients with respect to <code>params</code>. Results are unchanged.</li>
</ul>